Skip to main content

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