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 precision = if let Some(precision) = precision {
186        let rounded = precision.n.round();
187        if !(0.0..=9.0).contains(&rounded) {
188            return Err(KclError::new_semantic(KclErrorDetails::new(
189                "Precision must be between 0 and 9".to_owned(),
190                vec![args.source_range],
191            )));
192        }
193        rounded as u32
194    } else {
195        // The default precision.
196        3
197    };
198    let frame_plane = if let Some(plane) = frame_plane {
199        plane
200    } else {
201        // No plane given. Use one of the standard planes.
202        xy_plane(exec_state, args).await?
203    };
204    let frame_plane_id = if frame_plane.value == crate::exec::PlaneType::Uninit {
205        // Create it in the engine.
206        let engine_plane =
207            make_sketch_plane_from_orientation(frame_plane.info.into_plane_data(), exec_state, args).await?;
208        engine_plane.id
209    } else {
210        frame_plane.id
211    };
212    let mut annotations = Vec::with_capacity(faces.len());
213    for face in &faces {
214        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
215        let meta = vec![Metadata::from(args.source_range)];
216        let annotation_id = exec_state.next_uuid();
217        exec_state
218            .batch_modeling_cmd(
219                ModelingCmdMeta::from_args_id(args, annotation_id),
220                ModelingCmd::from(mcmd::NewAnnotation {
221                    options: AnnotationOptions {
222                        text: None,
223                        line_ends: None,
224                        line_width: None,
225                        color: None,
226                        position: None,
227                        dimension: None,
228                        feature_control: Some(AnnotationFeatureControl {
229                            entity_id: face_id,
230                            // Point to the center of the face.
231                            entity_pos: KPoint2d { x: 0.5, y: 0.5 },
232                            leader_type: AnnotationLineEnd::Dot,
233                            dimension: None,
234                            control_frame: Some(AnnotationMbdControlFrame {
235                                symbol: MbdSymbol::Flatness,
236                                diameter_symbol: None,
237                                tolerance: tolerance.to_mm(),
238                                modifier: None,
239                                primary_datum: None,
240                                secondary_datum: None,
241                                tertiary_datum: None,
242                            }),
243                            defined_datum: None,
244                            prefix: None,
245                            suffix: None,
246                            plane_id: frame_plane_id,
247                            offset: if let Some(offset) = &frame_position {
248                                KPoint2d {
249                                    x: offset[0].to_mm(),
250                                    y: offset[1].to_mm(),
251                                }
252                            } else {
253                                KPoint2d { x: 100.0, y: 100.0 }
254                            },
255                            precision,
256                            font_scale: style.font_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0),
257                            font_point_size: style.font_point_size.as_ref().map(|n| n.n.round() as u32).unwrap_or(36),
258                        }),
259                        feature_tag: None,
260                    },
261                    clobber: false,
262                    annotation_type: AnnotationType::T3D,
263                }),
264            )
265            .await?;
266        annotations.push(GdtAnnotation {
267            id: annotation_id,
268            meta,
269        });
270    }
271    Ok(annotations)
272}
273
274/// Get the XY plane by evaluating the `XY` expression so that it's the same as
275/// if the user specified `XY`.
276async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
277    let plane_ast = plane_ast("XY", args.source_range);
278    let metadata = Metadata::from(args.source_range);
279    let plane_value = args
280        .ctx
281        .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
282        .await?
283        .clone();
284    Ok(plane_value
285        .as_plane()
286        .ok_or_else(|| {
287            KclError::new_internal(KclErrorDetails::new(
288                "Expected XY plane to be defined".to_owned(),
289                vec![args.source_range],
290            ))
291        })?
292        .clone())
293}
294
295/// An AST node for a plane with the given name.
296fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
297    ast::Node::new(
298        ast::Expr::Name(Box::new(ast::Node::new(
299            ast::Name {
300                name: ast::Identifier::new(plane_name),
301                path: Vec::new(),
302                // TODO: We may want to set this to true once we implement it to
303                // prevent it breaking if users redefine the identifier.
304                abs_path: false,
305                digest: None,
306            },
307            range.start(),
308            range.end(),
309            range.module_id(),
310        ))),
311        range.start(),
312        range.end(),
313        range.module_id(),
314    )
315}