Skip to main content

kcl_lib/std/
gdt.rs

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