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::AnnotationBasicDimension;
5use kittycad_modeling_cmds::shared::AnnotationFeatureControl;
6use kittycad_modeling_cmds::shared::AnnotationLineEnd;
7use kittycad_modeling_cmds::shared::AnnotationMbdBasicDimension;
8use kittycad_modeling_cmds::shared::AnnotationMbdControlFrame;
9use kittycad_modeling_cmds::shared::AnnotationOptions;
10use kittycad_modeling_cmds::shared::AnnotationType;
11use kittycad_modeling_cmds::shared::MbdSymbol;
12use kittycad_modeling_cmds::shared::Point2d as KPoint2d;
13use kittycad_modeling_cmds::{self as kcmc};
14
15use crate::ExecState;
16use crate::KclError;
17use crate::errors::KclErrorDetails;
18use crate::exec::KclValue;
19use crate::execution::Artifact;
20use crate::execution::ArtifactId;
21use crate::execution::CodeRef;
22use crate::execution::ControlFlowKind;
23use crate::execution::Face;
24use crate::execution::GdtAnnotation;
25use crate::execution::GdtAnnotationArtifact;
26use crate::execution::Metadata;
27use crate::execution::ModelingCmdMeta;
28use crate::execution::Plane;
29use crate::execution::StatementKind;
30use crate::execution::TagIdentifier;
31use crate::execution::types::ArrayLen;
32use crate::execution::types::RuntimeType;
33use crate::parsing::ast::types as ast;
34use crate::std::Args;
35use crate::std::args::FromKclValue;
36use crate::std::args::TyF64;
37use crate::std::fillet::EdgeReference;
38use crate::std::sketch::ensure_sketch_plane_in_engine;
39
40// The engine exposes two text knobs:
41// - font_point_size controls the FreeType raster/bitmap texture resolution in pixels/points.
42// - font_scale is the unitless model-space multiplier applied to that texture.
43// KCL exposes only fontSize as a Length. Keep the raster quality fixed so changing
44// quality does not resize the text, and map the requested length into font_scale.
45const GDT_FONT_TEXTURE_POINT_SIZE: u32 = 36;
46const DEFAULT_GDT_FONT_SIZE_MM: f64 = 10.0;
47const DEFAULT_GDT_DOT_LEADER_SCALE: f64 = 1.0;
48const DEFAULT_GDT_DIMENSION_LEADER_SCALE: f64 = 1.0;
49const GDT_DOT_LEADER_REFERENCE_FONT_SIZE_MM: f64 = 100.0;
50const GDT_DOT_LEADER_REFERENCE_ENGINE_SCALE: f64 = 0.5;
51
52// Calibration target: measured annotation text/frame height in millimeters when
53// font_scale is 1.0 and GDT_FONT_TEXTURE_POINT_SIZE is fixed. Tune this value from
54// scene measurements, not by exposing engine font_point_size to users.
55const GDT_FONT_SCALE_1_HEIGHT_MM: f64 = 8.0;
56
57fn gdt_font_scale(font_size: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
58    let requested_height_mm = font_size.map(TyF64::to_mm).unwrap_or(DEFAULT_GDT_FONT_SIZE_MM);
59    if requested_height_mm <= 0.0 {
60        return Err(KclError::new_semantic(KclErrorDetails::new(
61            "fontSize must be greater than 0.".to_owned(),
62            vec![args.source_range],
63        )));
64    }
65    Ok(gdt_font_scale_for_height_mm(requested_height_mm))
66}
67
68fn gdt_font_scale_for_height_mm(requested_height_mm: f64) -> f32 {
69    (requested_height_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32
70}
71
72fn gdt_user_leader_scale(leader_scale: Option<&TyF64>, default_scale: f64, args: &Args) -> Result<f32, KclError> {
73    let scale = leader_scale.map(|scale| scale.n).unwrap_or(default_scale);
74    if scale <= 0.0 {
75        return Err(KclError::new_semantic(KclErrorDetails::new(
76            "leaderScale must be greater than 0.".to_owned(),
77            vec![args.source_range],
78        )));
79    }
80    Ok(scale as f32)
81}
82
83fn gdt_dot_leader_scale(leader_scale: Option<&TyF64>, font_size: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
84    let user_scale = gdt_user_leader_scale(leader_scale, DEFAULT_GDT_DOT_LEADER_SCALE, args)?;
85    // Engine dot leaders are screen-space point sprites after an internal font_scale
86    // multiplier. Divide that out so KCL leaderScale stays stable across fontSize.
87    Ok(user_scale * gdt_dot_leader_normal_size() / gdt_font_scale(font_size, args)?)
88}
89
90fn gdt_dot_leader_normal_size() -> f32 {
91    gdt_font_scale_for_height_mm(GDT_DOT_LEADER_REFERENCE_FONT_SIZE_MM) * GDT_DOT_LEADER_REFERENCE_ENGINE_SCALE as f32
92}
93
94fn gdt_dimension_leader_scale(leader_scale: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
95    gdt_user_leader_scale(leader_scale, DEFAULT_GDT_DIMENSION_LEADER_SCALE, args)
96}
97
98#[derive(Debug, Clone)]
99enum DistanceEntity {
100    Face(Box<Face>),
101    TaggedFace(Box<TagIdentifier>),
102    Edge(EdgeReference),
103}
104
105#[derive(Debug, Clone, Copy)]
106struct DistanceEndpoint {
107    entity_id: uuid::Uuid,
108    entity_pos: KPoint2d<f64>,
109}
110
111#[derive(Debug, Clone, Copy)]
112enum GdtFeatureControlKind {
113    Flatness,
114    Straightness,
115    Circularity,
116    Cylindricity,
117    Profile,
118    Position,
119    Angularity,
120    Perpendicularity,
121    Parallelism,
122}
123
124struct GdtFeatureControlParams {
125    faces: Vec<TagIdentifier>,
126    edges: Vec<EdgeReference>,
127    datums: Option<Vec<String>>,
128    tolerance: TyF64,
129    precision: Option<TyF64>,
130    frame_position: Option<[TyF64; 2]>,
131    frame_plane: Option<Plane>,
132    leader_scale: Option<TyF64>,
133    font_size: Option<TyF64>,
134}
135
136impl GdtFeatureControlKind {
137    fn label(self) -> &'static str {
138        match self {
139            Self::Flatness => "Flatness",
140            Self::Straightness => "Straightness",
141            Self::Circularity => "Circularity",
142            Self::Cylindricity => "Cylindricity",
143            Self::Profile => "Profile",
144            Self::Position => "Position",
145            Self::Angularity => "Angularity",
146            Self::Perpendicularity => "Perpendicularity",
147            Self::Parallelism => "Parallelism",
148        }
149    }
150
151    fn symbol(self) -> MbdSymbol {
152        match self {
153            Self::Flatness => MbdSymbol::Flatness,
154            Self::Straightness => MbdSymbol::Straightness,
155            Self::Circularity => MbdSymbol::Roundness,
156            Self::Cylindricity => MbdSymbol::Cylindricity,
157            Self::Profile => MbdSymbol::ProfileOfLine,
158            Self::Position => MbdSymbol::Position,
159            Self::Angularity => MbdSymbol::Angularity,
160            Self::Perpendicularity => MbdSymbol::Perpendicularity,
161            Self::Parallelism => MbdSymbol::Parallelism,
162        }
163    }
164}
165
166fn add_gdt_annotation_artifact(exec_state: &mut ExecState, args: &Args, annotation_id: uuid::Uuid) {
167    exec_state.add_artifact(Artifact::GdtAnnotation(GdtAnnotationArtifact {
168        id: ArtifactId::new(annotation_id),
169        code_ref: CodeRef::placeholder(args.source_range),
170    }));
171}
172
173impl DistanceEntity {
174    async fn to_endpoint(&self, exec_state: &mut ExecState, args: &Args) -> Result<DistanceEndpoint, KclError> {
175        match self {
176            DistanceEntity::Face(face) => Ok(DistanceEndpoint {
177                entity_id: face.id,
178                entity_pos: KPoint2d { x: 0.5, y: 0.5 },
179            }),
180            DistanceEntity::TaggedFace(face) => Ok(DistanceEndpoint {
181                entity_id: args.get_adjacent_face_to_tag(exec_state, face, false).await?,
182                entity_pos: KPoint2d { x: 0.5, y: 0.5 },
183            }),
184            DistanceEntity::Edge(edge) => Ok(DistanceEndpoint {
185                entity_id: edge.get_engine_id(exec_state, args)?,
186                entity_pos: KPoint2d { x: 0.5, y: 0.0 },
187            }),
188        }
189    }
190}
191
192impl<'a> FromKclValue<'a> for DistanceEntity {
193    fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
194        match arg {
195            KclValue::Face { value } => Some(Self::Face(value.to_owned())),
196            KclValue::Uuid { value, .. } => Some(Self::Edge(EdgeReference::Uuid(*value))),
197            KclValue::TagIdentifier(value) => Some(Self::TaggedFace(value.to_owned())),
198            _ => None,
199        }
200    }
201}
202
203fn distance_entity_type() -> RuntimeType {
204    RuntimeType::Union(vec![
205        RuntimeType::face(),
206        RuntimeType::tagged_face(),
207        RuntimeType::edge(),
208    ])
209}
210
211pub async fn datum(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
212    let face: TagIdentifier = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
213    let name: String = args.get_kw_arg("name", &RuntimeType::string(), exec_state)?;
214    let frame_position: Option<[TyF64; 2]> =
215        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
216    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
217    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
218    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
219
220    let annotation = inner_datum(
221        face,
222        name,
223        frame_position,
224        frame_plane,
225        leader_scale,
226        font_size,
227        exec_state,
228        &args,
229    )
230    .await?;
231    Ok(KclValue::GdtAnnotation {
232        value: Box::new(annotation),
233    })
234}
235
236#[allow(clippy::too_many_arguments)]
237async fn inner_datum(
238    face: TagIdentifier,
239    name: String,
240    frame_position: Option<[TyF64; 2]>,
241    frame_plane: Option<Plane>,
242    leader_scale: Option<TyF64>,
243    font_size: Option<TyF64>,
244    exec_state: &mut ExecState,
245    args: &Args,
246) -> Result<GdtAnnotation, KclError> {
247    const DATUM_LENGTH_ERROR: &str = "Datum name must be a single character.";
248    if name.len() > 1 {
249        return Err(KclError::new_semantic(KclErrorDetails::new(
250            DATUM_LENGTH_ERROR.to_owned(),
251            vec![args.source_range],
252        )));
253    }
254    let name_char = name.chars().next().ok_or_else(|| {
255        KclError::new_semantic(KclErrorDetails::new(
256            DATUM_LENGTH_ERROR.to_owned(),
257            vec![args.source_range],
258        ))
259    })?;
260    let mut frame_plane = if let Some(plane) = frame_plane {
261        plane
262    } else {
263        // No plane given. Use one of the standard planes.
264        xy_plane(exec_state, args).await?
265    };
266    ensure_sketch_plane_in_engine(
267        &mut frame_plane,
268        exec_state,
269        &args.ctx,
270        args.source_range,
271        args.node_path.clone(),
272    )
273    .await?;
274    let face_id = args.get_adjacent_face_to_tag(exec_state, &face, false).await?;
275    let meta = vec![Metadata::from(args.source_range)];
276    let annotation_id = exec_state.next_uuid();
277    let feature_control = AnnotationFeatureControl::builder()
278        .entity_id(face_id)
279        // Point to the center of the face.
280        .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
281        .leader_type(AnnotationLineEnd::Dot)
282        .defined_datum(name_char)
283        .plane_id(frame_plane.id)
284        .offset(if let Some(offset) = &frame_position {
285            KPoint2d {
286                x: offset[0].to_mm(),
287                y: offset[1].to_mm(),
288            }
289        } else {
290            KPoint2d { x: 100.0, y: 100.0 }
291        })
292        .precision(0)
293        .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
294        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
295        .leader_scale(gdt_dot_leader_scale(leader_scale.as_ref(), font_size.as_ref(), args)?)
296        .build();
297    exec_state
298        .batch_modeling_cmd(
299            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
300            ModelingCmd::from(
301                mcmd::NewAnnotation::builder()
302                    .options(AnnotationOptions::builder().feature_control(feature_control).build())
303                    .clobber(false)
304                    .annotation_type(AnnotationType::T3D)
305                    .build(),
306            ),
307        )
308        .await?;
309    add_gdt_annotation_artifact(exec_state, args, annotation_id);
310    Ok(GdtAnnotation {
311        id: annotation_id,
312        meta,
313    })
314}
315
316pub async fn flatness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
317    let faces: Vec<TagIdentifier> = args.get_kw_arg(
318        "faces",
319        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
320        exec_state,
321    )?;
322    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
323    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
324    let frame_position: Option<[TyF64; 2]> =
325        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
326    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
327    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
328    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
329
330    let annotations = create_feature_control_annotations(
331        GdtFeatureControlKind::Flatness,
332        GdtFeatureControlParams {
333            faces,
334            edges: Vec::new(),
335            datums: None,
336            tolerance,
337            precision,
338            frame_position,
339            frame_plane,
340            leader_scale,
341            font_size,
342        },
343        exec_state,
344        &args,
345    )
346    .await?;
347    Ok(annotations.into())
348}
349
350pub async fn straightness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
351    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
352        "faces",
353        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
354        exec_state,
355    )?;
356    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
357        "edges",
358        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
359        exec_state,
360    )?;
361    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
362    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
363    let frame_position: Option<[TyF64; 2]> =
364        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
365    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
366    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
367    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
368
369    let annotations = create_feature_control_annotations(
370        GdtFeatureControlKind::Straightness,
371        GdtFeatureControlParams {
372            faces: faces.unwrap_or_default(),
373            edges: edges.unwrap_or_default(),
374            datums: None,
375            tolerance,
376            precision,
377            frame_position,
378            frame_plane,
379            leader_scale,
380            font_size,
381        },
382        exec_state,
383        &args,
384    )
385    .await?;
386    Ok(annotations.into())
387}
388
389pub async fn circularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
390    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
391        "faces",
392        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
393        exec_state,
394    )?;
395    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
396        "edges",
397        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
398        exec_state,
399    )?;
400    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
401    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
402    let frame_position: Option<[TyF64; 2]> =
403        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
404    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
405    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
406    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
407
408    let annotations = create_feature_control_annotations(
409        GdtFeatureControlKind::Circularity,
410        GdtFeatureControlParams {
411            faces: faces.unwrap_or_default(),
412            edges: edges.unwrap_or_default(),
413            datums: None,
414            tolerance,
415            precision,
416            frame_position,
417            frame_plane,
418            leader_scale,
419            font_size,
420        },
421        exec_state,
422        &args,
423    )
424    .await?;
425    Ok(annotations.into())
426}
427
428pub async fn cylindricity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
429    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
430        "faces",
431        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
432        exec_state,
433    )?;
434    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
435        "edges",
436        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
437        exec_state,
438    )?;
439    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
440    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
441    let frame_position: Option<[TyF64; 2]> =
442        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
443    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
444    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
445    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
446
447    let annotations = create_feature_control_annotations(
448        GdtFeatureControlKind::Cylindricity,
449        GdtFeatureControlParams {
450            faces: faces.unwrap_or_default(),
451            edges: edges.unwrap_or_default(),
452            datums: None,
453            tolerance,
454            precision,
455            frame_position,
456            frame_plane,
457            leader_scale,
458            font_size,
459        },
460        exec_state,
461        &args,
462    )
463    .await?;
464    Ok(annotations.into())
465}
466
467pub async fn profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
468    let edges: Vec<EdgeReference> = args.get_kw_arg(
469        "edges",
470        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
471        exec_state,
472    )?;
473    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
474        "datums",
475        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
476        exec_state,
477    )?;
478    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
479    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
480    let frame_position: Option<[TyF64; 2]> =
481        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
482    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
483    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
484    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
485
486    let annotations = create_feature_control_annotations(
487        GdtFeatureControlKind::Profile,
488        GdtFeatureControlParams {
489            faces: Vec::new(),
490            edges,
491            datums,
492            tolerance,
493            precision,
494            frame_position,
495            frame_plane,
496            leader_scale,
497            font_size,
498        },
499        exec_state,
500        &args,
501    )
502    .await?;
503    Ok(annotations.into())
504}
505
506pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
507    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
508        "faces",
509        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
510        exec_state,
511    )?;
512    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
513        "edges",
514        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
515        exec_state,
516    )?;
517    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
518        "datums",
519        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
520        exec_state,
521    )?;
522    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
523    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
524    let frame_position: Option<[TyF64; 2]> =
525        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
526    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
527    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
528    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
529
530    let annotations = create_feature_control_annotations(
531        GdtFeatureControlKind::Position,
532        GdtFeatureControlParams {
533            faces: faces.unwrap_or_default(),
534            edges: edges.unwrap_or_default(),
535            datums,
536            tolerance,
537            precision,
538            frame_position,
539            frame_plane,
540            leader_scale,
541            font_size,
542        },
543        exec_state,
544        &args,
545    )
546    .await?;
547    Ok(annotations.into())
548}
549
550pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
551    let from: Option<DistanceEntity> = args.get_kw_arg_opt("from", &distance_entity_type(), exec_state)?;
552    let to: Option<DistanceEntity> = args.get_kw_arg_opt("to", &distance_entity_type(), exec_state)?;
553    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
554        "edges",
555        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
556        exec_state,
557    )?;
558    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
559    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
560    let frame_position: Option<[TyF64; 2]> =
561        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
562    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
563    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
564    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
565
566    let annotations = inner_distance(
567        from,
568        to,
569        edges.unwrap_or_default(),
570        tolerance,
571        precision,
572        frame_position,
573        frame_plane,
574        leader_scale,
575        font_size,
576        exec_state,
577        &args,
578    )
579    .await?;
580    Ok(annotations.into())
581}
582
583#[allow(clippy::too_many_arguments)]
584async fn inner_distance(
585    from: Option<DistanceEntity>,
586    to: Option<DistanceEntity>,
587    edges: Vec<EdgeReference>,
588    tolerance: TyF64,
589    precision: Option<TyF64>,
590    frame_position: Option<[TyF64; 2]>,
591    frame_plane: Option<Plane>,
592    leader_scale: Option<TyF64>,
593    font_size: Option<TyF64>,
594    exec_state: &mut ExecState,
595    args: &Args,
596) -> Result<Vec<GdtAnnotation>, KclError> {
597    let precision = resolve_precision(precision, args)?;
598    let mut frame_plane = if let Some(plane) = frame_plane {
599        plane
600    } else {
601        xy_plane(exec_state, args).await?
602    };
603    ensure_sketch_plane_in_engine(
604        &mut frame_plane,
605        exec_state,
606        &args.ctx,
607        args.source_range,
608        args.node_path.clone(),
609    )
610    .await?;
611
612    if from.is_some() || to.is_some() {
613        if !edges.is_empty() {
614            return Err(KclError::new_semantic(KclErrorDetails::new(
615                "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
616                vec![args.source_range],
617            )));
618        }
619
620        let (Some(from), Some(to)) = (from, to) else {
621            return Err(KclError::new_semantic(KclErrorDetails::new(
622                "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
623                vec![args.source_range],
624            )));
625        };
626
627        let from = from.to_endpoint(exec_state, args).await?;
628        let to = to.to_endpoint(exec_state, args).await?;
629        let mut annotations = Vec::with_capacity(1);
630        create_basic_distance_annotation(
631            from,
632            to,
633            &tolerance,
634            precision,
635            frame_position.as_ref(),
636            frame_plane.id,
637            leader_scale.as_ref(),
638            font_size.as_ref(),
639            exec_state,
640            args,
641            &mut annotations,
642        )
643        .await?;
644        return Ok(annotations);
645    }
646
647    if edges.is_empty() {
648        return Err(KclError::new_semantic(KclErrorDetails::new(
649            "Distance requires either `edges` or both `from` and `to`.".to_owned(),
650            vec![args.source_range],
651        )));
652    }
653
654    let mut annotations = Vec::with_capacity(edges.len());
655    for edge in &edges {
656        let edge_id = edge.get_engine_id(exec_state, args)?;
657        create_basic_distance_annotation(
658            DistanceEndpoint {
659                entity_id: edge_id,
660                entity_pos: KPoint2d { x: 0.0, y: 0.0 },
661            },
662            DistanceEndpoint {
663                entity_id: edge_id,
664                entity_pos: KPoint2d { x: 1.0, y: 0.0 },
665            },
666            &tolerance,
667            precision,
668            frame_position.as_ref(),
669            frame_plane.id,
670            leader_scale.as_ref(),
671            font_size.as_ref(),
672            exec_state,
673            args,
674            &mut annotations,
675        )
676        .await?;
677    }
678    Ok(annotations)
679}
680
681#[allow(clippy::too_many_arguments)]
682async fn create_basic_distance_annotation(
683    from: DistanceEndpoint,
684    to: DistanceEndpoint,
685    tolerance: &TyF64,
686    precision: u32,
687    frame_position: Option<&[TyF64; 2]>,
688    frame_plane_id: uuid::Uuid,
689    leader_scale: Option<&TyF64>,
690    font_size: Option<&TyF64>,
691    exec_state: &mut ExecState,
692    args: &Args,
693    annotations: &mut Vec<GdtAnnotation>,
694) -> Result<(), KclError> {
695    let meta = vec![Metadata::from(args.source_range)];
696    let annotation_id = exec_state.next_uuid();
697    let display_units = exec_state.length_unit();
698    let dimension = AnnotationBasicDimension::builder()
699        .from_entity_id(from.entity_id)
700        .from_entity_pos(from.entity_pos)
701        .to_entity_id(to.entity_id)
702        .to_entity_pos(to.entity_pos)
703        .dimension(
704            AnnotationMbdBasicDimension::builder()
705                .tolerance(tolerance.to_length_units(display_units))
706                .build(),
707        )
708        .plane_id(frame_plane_id)
709        .offset(if let Some(offset) = frame_position {
710            KPoint2d {
711                x: offset[0].to_mm(),
712                y: offset[1].to_mm(),
713            }
714        } else {
715            KPoint2d { x: 100.0, y: 100.0 }
716        })
717        .precision(precision)
718        .font_scale(gdt_font_scale(font_size, args)?)
719        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
720        .arrow_scale(gdt_dimension_leader_scale(leader_scale, args)?)
721        .build();
722    let options = AnnotationOptions::builder()
723        .dimension(dimension)
724        .units(display_units)
725        .build();
726    let annotation_cmd = ModelingCmd::from(
727        mcmd::NewAnnotation::builder()
728            .options(options)
729            .clobber(false)
730            .annotation_type(AnnotationType::T3D)
731            .build(),
732    );
733    let cmd_meta = ModelingCmdMeta::from_args_id(exec_state, args, annotation_id);
734    exec_state.batch_modeling_cmd(cmd_meta, annotation_cmd).await?;
735    add_gdt_annotation_artifact(exec_state, args, annotation_id);
736    annotations.push(GdtAnnotation {
737        id: annotation_id,
738        meta,
739    });
740    Ok(())
741}
742
743pub async fn angularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
744    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
745        "faces",
746        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
747        exec_state,
748    )?;
749    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
750        "edges",
751        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
752        exec_state,
753    )?;
754    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
755        "datums",
756        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
757        exec_state,
758    )?;
759    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
760    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
761    let frame_position: Option<[TyF64; 2]> =
762        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
763    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
764    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
765    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
766
767    let annotations = create_feature_control_annotations(
768        GdtFeatureControlKind::Angularity,
769        GdtFeatureControlParams {
770            faces: faces.unwrap_or_default(),
771            edges: edges.unwrap_or_default(),
772            datums,
773            tolerance,
774            precision,
775            frame_position,
776            frame_plane,
777            leader_scale,
778            font_size,
779        },
780        exec_state,
781        &args,
782    )
783    .await?;
784    Ok(annotations.into())
785}
786
787pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
788    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
789        "faces",
790        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
791        exec_state,
792    )?;
793    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
794        "edges",
795        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
796        exec_state,
797    )?;
798    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
799        "datums",
800        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
801        exec_state,
802    )?;
803    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
804    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
805    let frame_position: Option<[TyF64; 2]> =
806        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
807    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
808    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
809    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
810
811    let annotations = create_feature_control_annotations(
812        GdtFeatureControlKind::Perpendicularity,
813        GdtFeatureControlParams {
814            faces: faces.unwrap_or_default(),
815            edges: edges.unwrap_or_default(),
816            datums,
817            tolerance,
818            precision,
819            frame_position,
820            frame_plane,
821            leader_scale,
822            font_size,
823        },
824        exec_state,
825        &args,
826    )
827    .await?;
828    Ok(annotations.into())
829}
830
831pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
832    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
833        "faces",
834        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
835        exec_state,
836    )?;
837    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
838        "edges",
839        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
840        exec_state,
841    )?;
842    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
843        "datums",
844        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
845        exec_state,
846    )?;
847    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
848    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
849    let frame_position: Option<[TyF64; 2]> =
850        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
851    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
852    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
853    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
854
855    let annotations = create_feature_control_annotations(
856        GdtFeatureControlKind::Parallelism,
857        GdtFeatureControlParams {
858            faces: faces.unwrap_or_default(),
859            edges: edges.unwrap_or_default(),
860            datums,
861            tolerance,
862            precision,
863            frame_position,
864            frame_plane,
865            leader_scale,
866            font_size,
867        },
868        exec_state,
869        &args,
870    )
871    .await?;
872    Ok(annotations.into())
873}
874
875pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
876    let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
877    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
878        "faces",
879        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
880        exec_state,
881    )?;
882    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
883        "edges",
884        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
885        exec_state,
886    )?;
887    let frame_position: Option<[TyF64; 2]> =
888        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
889    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
890    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
891    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
892
893    let annotations = inner_annotation(
894        annotation,
895        faces.unwrap_or_default(),
896        edges.unwrap_or_default(),
897        frame_position,
898        frame_plane,
899        leader_scale,
900        font_size,
901        exec_state,
902        &args,
903    )
904    .await?;
905    Ok(annotations.into())
906}
907
908#[allow(clippy::too_many_arguments)]
909async fn inner_annotation(
910    annotation: String,
911    faces: Vec<TagIdentifier>,
912    edges: Vec<EdgeReference>,
913    frame_position: Option<[TyF64; 2]>,
914    frame_plane: Option<Plane>,
915    leader_scale: Option<TyF64>,
916    font_size: Option<TyF64>,
917    exec_state: &mut ExecState,
918    args: &Args,
919) -> Result<Vec<GdtAnnotation>, KclError> {
920    if annotation.is_empty() {
921        return Err(KclError::new_semantic(KclErrorDetails::new(
922            "Annotation text must not be empty.".to_owned(),
923            vec![args.source_range],
924        )));
925    }
926    if faces.is_empty() && edges.is_empty() {
927        return Err(KclError::new_semantic(KclErrorDetails::new(
928            "Annotation requires at least one face or edge.".to_owned(),
929            vec![args.source_range],
930        )));
931    }
932
933    let mut frame_plane = if let Some(plane) = frame_plane {
934        plane
935    } else {
936        xy_plane(exec_state, args).await?
937    };
938    ensure_sketch_plane_in_engine(
939        &mut frame_plane,
940        exec_state,
941        &args.ctx,
942        args.source_range,
943        args.node_path.clone(),
944    )
945    .await?;
946
947    let mut annotations = Vec::with_capacity(faces.len() + edges.len());
948    for face in &faces {
949        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
950        create_annotation(
951            face_id,
952            &annotation,
953            frame_position.as_ref(),
954            frame_plane.id,
955            leader_scale.as_ref(),
956            font_size.as_ref(),
957            exec_state,
958            args,
959            &mut annotations,
960        )
961        .await?;
962    }
963    for edge in &edges {
964        let edge_id = edge.get_engine_id(exec_state, args)?;
965        create_annotation(
966            edge_id,
967            &annotation,
968            frame_position.as_ref(),
969            frame_plane.id,
970            leader_scale.as_ref(),
971            font_size.as_ref(),
972            exec_state,
973            args,
974            &mut annotations,
975        )
976        .await?;
977    }
978
979    Ok(annotations)
980}
981
982fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
983    if let Some(precision) = precision {
984        let rounded = precision.n.round();
985        if !(0.0..=9.0).contains(&rounded) {
986            return Err(KclError::new_semantic(KclErrorDetails::new(
987                "Precision must be between 0 and 9".to_owned(),
988                vec![args.source_range],
989            )));
990        }
991        Ok(rounded as u32)
992    } else {
993        Ok(3)
994    }
995}
996
997async fn resolve_gdt_frame_plane(
998    frame_plane: Option<Plane>,
999    exec_state: &mut ExecState,
1000    args: &Args,
1001) -> Result<Plane, KclError> {
1002    let mut frame_plane = if let Some(plane) = frame_plane {
1003        plane
1004    } else {
1005        // No plane given. Use one of the standard planes.
1006        xy_plane(exec_state, args).await?
1007    };
1008    ensure_sketch_plane_in_engine(
1009        &mut frame_plane,
1010        exec_state,
1011        &args.ctx,
1012        args.source_range,
1013        args.node_path.clone(),
1014    )
1015    .await?;
1016    Ok(frame_plane)
1017}
1018
1019async fn create_feature_control_annotations(
1020    kind: GdtFeatureControlKind,
1021    params: GdtFeatureControlParams,
1022    exec_state: &mut ExecState,
1023    args: &Args,
1024) -> Result<Vec<GdtAnnotation>, KclError> {
1025    let GdtFeatureControlParams {
1026        faces,
1027        edges,
1028        datums,
1029        tolerance,
1030        precision,
1031        frame_position,
1032        frame_plane,
1033        leader_scale,
1034        font_size,
1035    } = params;
1036
1037    if faces.is_empty() && edges.is_empty() {
1038        return Err(KclError::new_semantic(KclErrorDetails::new(
1039            format!("{} requires at least one face or edge.", kind.label()),
1040            vec![args.source_range],
1041        )));
1042    }
1043
1044    let precision = resolve_precision(precision, args)?;
1045    let datums = resolve_datums(datums, args, kind.label())?;
1046    let frame_plane = resolve_gdt_frame_plane(frame_plane, exec_state, args).await?;
1047    let symbol = kind.symbol();
1048
1049    let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1050    for face in &faces {
1051        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1052        create_feature_control_annotation(
1053            face_id,
1054            symbol,
1055            &tolerance,
1056            &datums,
1057            precision,
1058            frame_position.as_ref(),
1059            frame_plane.id,
1060            leader_scale.as_ref(),
1061            font_size.as_ref(),
1062            exec_state,
1063            args,
1064            &mut annotations,
1065        )
1066        .await?;
1067    }
1068    for edge in &edges {
1069        let edge_id = edge.get_engine_id(exec_state, args)?;
1070        create_feature_control_annotation(
1071            edge_id,
1072            symbol,
1073            &tolerance,
1074            &datums,
1075            precision,
1076            frame_position.as_ref(),
1077            frame_plane.id,
1078            leader_scale.as_ref(),
1079            font_size.as_ref(),
1080            exec_state,
1081            args,
1082            &mut annotations,
1083        )
1084        .await?;
1085    }
1086
1087    Ok(annotations)
1088}
1089
1090#[allow(clippy::too_many_arguments)]
1091async fn create_feature_control_annotation(
1092    entity_id: uuid::Uuid,
1093    symbol: MbdSymbol,
1094    tolerance: &TyF64,
1095    datums: &[char],
1096    precision: u32,
1097    frame_position: Option<&[TyF64; 2]>,
1098    frame_plane_id: uuid::Uuid,
1099    leader_scale: Option<&TyF64>,
1100    font_size: Option<&TyF64>,
1101    exec_state: &mut ExecState,
1102    args: &Args,
1103    annotations: &mut Vec<GdtAnnotation>,
1104) -> Result<(), KclError> {
1105    let meta = vec![Metadata::from(args.source_range)];
1106    let annotation_id = exec_state.next_uuid();
1107    let display_units = exec_state.length_unit();
1108    let control_frame = gdt_control_frame(symbol, tolerance.to_length_units(display_units), datums);
1109    let feature_control = AnnotationFeatureControl::builder()
1110        .entity_id(entity_id)
1111        .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1112        .leader_type(AnnotationLineEnd::Dot)
1113        .control_frame(control_frame)
1114        .plane_id(frame_plane_id)
1115        .offset(if let Some(offset) = frame_position {
1116            KPoint2d {
1117                x: offset[0].to_mm(),
1118                y: offset[1].to_mm(),
1119            }
1120        } else {
1121            KPoint2d { x: 100.0, y: 100.0 }
1122        })
1123        .precision(precision)
1124        .font_scale(gdt_font_scale(font_size, args)?)
1125        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1126        .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1127        .build();
1128    let options = AnnotationOptions::builder().feature_control(feature_control).build();
1129    exec_state
1130        .batch_modeling_cmd(
1131            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1132            ModelingCmd::from(
1133                mcmd::NewAnnotation::builder()
1134                    .options(options)
1135                    .clobber(false)
1136                    .annotation_type(AnnotationType::T3D)
1137                    .build(),
1138            ),
1139        )
1140        .await?;
1141    add_gdt_annotation_artifact(exec_state, args, annotation_id);
1142    annotations.push(GdtAnnotation {
1143        id: annotation_id,
1144        meta,
1145    });
1146    Ok(())
1147}
1148
1149fn gdt_control_frame(symbol: MbdSymbol, tolerance: f64, datums: &[char]) -> AnnotationMbdControlFrame {
1150    match datums {
1151        [] => AnnotationMbdControlFrame::builder()
1152            .symbol(symbol)
1153            .tolerance(tolerance)
1154            .build(),
1155        [primary] => AnnotationMbdControlFrame::builder()
1156            .symbol(symbol)
1157            .tolerance(tolerance)
1158            .primary_datum(*primary)
1159            .build(),
1160        [primary, secondary] => AnnotationMbdControlFrame::builder()
1161            .symbol(symbol)
1162            .tolerance(tolerance)
1163            .primary_datum(*primary)
1164            .secondary_datum(*secondary)
1165            .build(),
1166        [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1167            .symbol(symbol)
1168            .tolerance(tolerance)
1169            .primary_datum(*primary)
1170            .secondary_datum(*secondary)
1171            .tertiary_datum(*tertiary)
1172            .build(),
1173        _ => unreachable!("resolve_datums rejects more than three datums"),
1174    }
1175}
1176
1177#[allow(clippy::too_many_arguments)]
1178async fn create_annotation(
1179    entity_id: uuid::Uuid,
1180    annotation: &str,
1181    frame_position: Option<&[TyF64; 2]>,
1182    frame_plane_id: uuid::Uuid,
1183    leader_scale: Option<&TyF64>,
1184    font_size: Option<&TyF64>,
1185    exec_state: &mut ExecState,
1186    args: &Args,
1187    annotations: &mut Vec<GdtAnnotation>,
1188) -> Result<(), KclError> {
1189    let meta = vec![Metadata::from(args.source_range)];
1190    let annotation_id = exec_state.next_uuid();
1191    let feature_control = AnnotationFeatureControl::builder()
1192        .entity_id(entity_id)
1193        .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1194        .leader_type(AnnotationLineEnd::Dot)
1195        .prefix(annotation.to_owned())
1196        .plane_id(frame_plane_id)
1197        .offset(if let Some(offset) = frame_position {
1198            KPoint2d {
1199                x: offset[0].to_mm(),
1200                y: offset[1].to_mm(),
1201            }
1202        } else {
1203            KPoint2d { x: 100.0, y: 100.0 }
1204        })
1205        .precision(0)
1206        .font_scale(gdt_font_scale(font_size, args)?)
1207        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1208        .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1209        .build();
1210    let options = AnnotationOptions::builder().feature_control(feature_control).build();
1211    exec_state
1212        .batch_modeling_cmd(
1213            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1214            ModelingCmd::from(
1215                mcmd::NewAnnotation::builder()
1216                    .options(options)
1217                    .clobber(false)
1218                    .annotation_type(AnnotationType::T3D)
1219                    .build(),
1220            ),
1221        )
1222        .await?;
1223    add_gdt_annotation_artifact(exec_state, args, annotation_id);
1224    annotations.push(GdtAnnotation {
1225        id: annotation_id,
1226        meta,
1227    });
1228    Ok(())
1229}
1230
1231fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1232    let datums = datums.unwrap_or_default();
1233    if datums.len() > 3 {
1234        return Err(KclError::new_semantic(KclErrorDetails::new(
1235            format!("{annotation_name} datums must include at most three names."),
1236            vec![args.source_range],
1237        )));
1238    }
1239
1240    let mut resolved = Vec::with_capacity(datums.len());
1241    for datum in &datums {
1242        let mut chars = datum.chars();
1243        let Some(name) = chars.next() else {
1244            return Err(KclError::new_semantic(KclErrorDetails::new(
1245                format!("{annotation_name} datum names must be a single character."),
1246                vec![args.source_range],
1247            )));
1248        };
1249        if chars.next().is_some() {
1250            return Err(KclError::new_semantic(KclErrorDetails::new(
1251                format!("{annotation_name} datum names must be a single character."),
1252                vec![args.source_range],
1253            )));
1254        }
1255        resolved.push(name);
1256    }
1257
1258    Ok(resolved)
1259}
1260
1261/// Get the XY plane by evaluating the `XY` expression so that it's the same as
1262/// if the user specified `XY`.
1263async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1264    let plane_ast = plane_ast("XY", args.source_range);
1265    let metadata = Metadata::from(args.source_range);
1266    let plane_value = args
1267        .ctx
1268        .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
1269        .await?;
1270    let plane_value = match plane_value.control {
1271        ControlFlowKind::Continue => plane_value.into_value(),
1272        ControlFlowKind::Exit => {
1273            let message = "Early return inside plane value is currently not supported".to_owned();
1274            debug_assert!(false, "{}", &message);
1275            return Err(KclError::new_internal(KclErrorDetails::new(
1276                message,
1277                vec![args.source_range],
1278            )));
1279        }
1280    };
1281    Ok(plane_value
1282        .as_plane()
1283        .ok_or_else(|| {
1284            KclError::new_internal(KclErrorDetails::new(
1285                "Expected XY plane to be defined".to_owned(),
1286                vec![args.source_range],
1287            ))
1288        })?
1289        .clone())
1290}
1291
1292/// An AST node for a plane with the given name.
1293fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1294    ast::Node::new(
1295        ast::Expr::Name(Box::new(ast::Node::new(
1296            ast::Name {
1297                name: ast::Identifier::new(plane_name),
1298                path: Vec::new(),
1299                // TODO: We may want to set this to true once we implement it to
1300                // prevent it breaking if users redefine the identifier.
1301                abs_path: false,
1302                digest: None,
1303            },
1304            range.start(),
1305            range.end(),
1306            range.module_id(),
1307        ))),
1308        range.start(),
1309        range.end(),
1310        range.module_id(),
1311    )
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use super::*;
1317    use crate::ExecutorContext;
1318    use crate::execution::Artifact;
1319    use crate::execution::ExecutorSettings;
1320    use crate::execution::MockConfig;
1321    use crate::execution::parse_execute;
1322
1323    const GDT_DISTANCE_KCL_TEMPLATE: &str = r#"
1324@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1325
1326sketch001 = sketch(on = XY) {
1327  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1328  line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1329  line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1330  line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1331  coincident([line1.end, line2.start])
1332  coincident([line2.end, line3.start])
1333  coincident([line3.end, line4.start])
1334  coincident([line4.end, line1.start])
1335  parallel([line2, line4])
1336  parallel([line3, line1])
1337  perpendicular([line1, line2])
1338  horizontal(line3)
1339}
1340
1341region001 = region(point = [5mm, 5mm], sketch = sketch001)
1342extrude001 = extrude(region001, length = 10mm)
1343gdt::distance(
1344  edges = [
1345    getCommonEdge(faces = [
1346      region001.tags.line4,
1347      region001.tags.line1
1348    ])
1349  ],
1350  tolerance = __TOLERANCE__,
1351  framePosition = __FRAME_POSITION__,
1352  fontSize = 2in,
1353)
1354"#;
1355
1356    const GDT_FLATNESS_KCL_TEMPLATE: &str = r#"
1357@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1358
1359sketch001 = sketch(on = XY) {
1360  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1361  line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1362  line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1363  line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1364  coincident([line1.end, line2.start])
1365  coincident([line2.end, line3.start])
1366  coincident([line3.end, line4.start])
1367  coincident([line4.end, line1.start])
1368  parallel([line2, line4])
1369  parallel([line3, line1])
1370  perpendicular([line1, line2])
1371  horizontal(line3)
1372}
1373
1374region001 = region(point = [5mm, 5mm], sketch = sketch001)
1375extrude001 = extrude(region001, length = 10mm, tagEnd = $capEnd001)
1376gdt::flatness(
1377  faces = [capEnd001],
1378  tolerance = __TOLERANCE__,
1379  framePosition = __FRAME_POSITION__,
1380  framePlane = XZ,
1381  fontSize = 2in,
1382)
1383"#;
1384
1385    fn gdt_distance_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1386        GDT_DISTANCE_KCL_TEMPLATE
1387            .replace("__UNIT__", unit)
1388            .replace("__TOLERANCE__", tolerance)
1389            .replace("__FRAME_POSITION__", frame_position)
1390    }
1391
1392    fn gdt_flatness_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1393        GDT_FLATNESS_KCL_TEMPLATE
1394            .replace("__UNIT__", unit)
1395            .replace("__TOLERANCE__", tolerance)
1396            .replace("__FRAME_POSITION__", frame_position)
1397    }
1398
1399    async fn gdt_commands(code: &str) -> Vec<ModelingCmd> {
1400        let result = parse_execute(code).await.unwrap();
1401        result
1402            .root_module_artifact_commands()
1403            .iter()
1404            .map(|artifact_command| artifact_command.command.clone())
1405            .collect()
1406    }
1407
1408    fn annotation_options(command: &ModelingCmd) -> Result<&AnnotationOptions, KclError> {
1409        let ModelingCmd::NewAnnotation(new_annotation) = command else {
1410            return Err(KclError::new_internal(KclErrorDetails::new(
1411                format!("expected new_annotation command, got {command:?}"),
1412                vec![SourceRange::default()],
1413            )));
1414        };
1415        Ok(&new_annotation.options)
1416    }
1417
1418    fn feature_control(command: &ModelingCmd) -> Result<&AnnotationFeatureControl, KclError> {
1419        let ModelingCmd::NewAnnotation(new_annotation) = command else {
1420            return Err(KclError::new_internal(KclErrorDetails::new(
1421                format!("expected new_annotation command, got {command:?}"),
1422                vec![SourceRange::default()],
1423            )));
1424        };
1425        new_annotation.options.feature_control.as_ref().ok_or_else(|| {
1426            KclError::new_internal(KclErrorDetails::new(
1427                "expected new_annotation command to have a feature_control".to_owned(),
1428                vec![SourceRange::default()],
1429            ))
1430        })
1431    }
1432
1433    fn find_control_frame_with_symbol(
1434        commands: &[ModelingCmd],
1435        symbol: MbdSymbol,
1436    ) -> Result<&AnnotationMbdControlFrame, KclError> {
1437        for command in commands {
1438            if let Ok(feature_control) = feature_control(command)
1439                && let Some(control_frame) = feature_control.control_frame.as_ref()
1440                && control_frame.symbol == symbol
1441            {
1442                return Ok(control_frame);
1443            }
1444        }
1445
1446        Err(KclError::new_internal(KclErrorDetails::new(
1447            format!("expected commands to contain a {symbol:?} control frame"),
1448            vec![SourceRange::default()],
1449        )))
1450    }
1451
1452    #[track_caller]
1453    fn assert_close(actual: f64, expected: f64) {
1454        assert!((actual - expected).abs() < 1e-6, "expected {expected}, got {actual}");
1455    }
1456
1457    fn new_annotation_command_index(commands: &[ModelingCmd]) -> Result<usize, KclError> {
1458        commands
1459            .iter()
1460            .position(|command| matches!(command, ModelingCmd::NewAnnotation(_)))
1461            .ok_or_else(|| {
1462                KclError::new_internal(KclErrorDetails::new(
1463                    "expected commands to contain a new_annotation command".to_owned(),
1464                    vec![SourceRange::default()],
1465                ))
1466            })
1467    }
1468
1469    #[test]
1470    fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1471        let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1472        assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1473
1474        let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1475        assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1476
1477        let inch_in_mm = 25.4;
1478        let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1479        assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1480    }
1481
1482    const GDT_FLATNESS_LEADER_KCL_TEMPLATE: &str = r#"
1483@settings(defaultLengthUnit = mm, kclVersion = 2)
1484
1485blockProfile = sketch(on = XY) {
1486  edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1487  edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1488  edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1489  edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1490  coincident([edge1.end, edge2.start])
1491  coincident([edge2.end, edge3.start])
1492  coincident([edge3.end, edge4.start])
1493  coincident([edge4.end, edge1.start])
1494  parallel([edge2, edge4])
1495  parallel([edge3, edge1])
1496  perpendicular([edge1, edge2])
1497  horizontal(edge3)
1498}
1499
1500region001 = region(point = [5mm, 5mm], sketch = blockProfile)
1501extrude001 = extrude(region001, length = 10mm, tagEnd = $top)
1502gdt::flatness(
1503  faces = [top],
1504  tolerance = 0.1mm,
1505  framePosition = [10mm, 0mm],
1506  framePlane = XZ,
1507  fontSize = __FONT_SIZE__
1508  __LEADER_SCALE__
1509)
1510"#;
1511
1512    fn gdt_flatness_leader_kcl(font_size: &str, leader_scale: Option<&str>) -> String {
1513        GDT_FLATNESS_LEADER_KCL_TEMPLATE
1514            .replace("__FONT_SIZE__", font_size)
1515            .replace(
1516                "__LEADER_SCALE__",
1517                leader_scale
1518                    .map(|scale| format!(",\n  leaderScale = {scale}"))
1519                    .unwrap_or_default()
1520                    .as_str(),
1521            )
1522    }
1523
1524    async fn gdt_flatness_feature_control(
1525        font_size: &str,
1526        leader_scale: Option<&str>,
1527    ) -> Result<AnnotationFeatureControl, KclError> {
1528        let code = gdt_flatness_leader_kcl(font_size, leader_scale);
1529        let commands = gdt_commands(&code).await;
1530        let annotation_index = new_annotation_command_index(&commands)?;
1531        Ok(feature_control(&commands[annotation_index])?.clone())
1532    }
1533
1534    #[tokio::test(flavor = "multi_thread")]
1535    async fn gdt_dot_leader_scale_is_normalized_against_font_scale() -> Result<(), KclError> {
1536        let tiny = gdt_flatness_feature_control("1mm", None).await?;
1537        let large = gdt_flatness_feature_control("100mm", None).await?;
1538
1539        assert_close(f64::from(tiny.font_scale), gdt_font_scale_for_height_mm(1.0).into());
1540        assert_close(f64::from(large.font_scale), gdt_font_scale_for_height_mm(100.0).into());
1541        assert_close(f64::from(tiny.leader_scale), 50.0);
1542        assert_close(f64::from(large.leader_scale), 0.5);
1543
1544        assert_close(
1545            f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1546            f64::from(gdt_dot_leader_normal_size()),
1547        );
1548        assert_close(
1549            f64::from(large.font_scale) * f64::from(large.leader_scale),
1550            f64::from(gdt_dot_leader_normal_size()),
1551        );
1552        Ok(())
1553    }
1554
1555    #[tokio::test(flavor = "multi_thread")]
1556    async fn explicit_gdt_dot_leader_scale_multiplies_normal_size() -> Result<(), KclError> {
1557        let tiny = gdt_flatness_feature_control("1mm", Some("2")).await?;
1558        let large = gdt_flatness_feature_control("100mm", Some("2")).await?;
1559
1560        let expected_scaled_dot_size = f64::from(gdt_dot_leader_normal_size()) * 2.0;
1561        assert_close(
1562            f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1563            expected_scaled_dot_size,
1564        );
1565        assert_close(
1566            f64::from(large.font_scale) * f64::from(large.leader_scale),
1567            expected_scaled_dot_size,
1568        );
1569        Ok(())
1570    }
1571
1572    #[tokio::test(flavor = "multi_thread")]
1573    async fn gdt_flatness_uses_scene_units_for_control_frame_tolerance() -> Result<(), KclError> {
1574        let cases = [
1575            ("in", "0.1in", "[10, -10]", 0.1, 254.0, -254.0),
1576            ("cm", "10mm", "[1, -1]", 1.0, 10.0, -10.0),
1577        ];
1578
1579        for (default_unit, tolerance, frame_position, expected_tolerance, expected_x, expected_y) in cases {
1580            let code = gdt_flatness_kcl(default_unit, tolerance, frame_position);
1581            let commands = gdt_commands(&code).await;
1582            let annotation_index = new_annotation_command_index(&commands)?;
1583            let feature_control = feature_control(&commands[annotation_index])?;
1584            let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1585                KclError::new_internal(KclErrorDetails::new(
1586                    "expected feature_control to have a control_frame".to_owned(),
1587                    vec![SourceRange::default()],
1588                ))
1589            })?;
1590
1591            assert_close(control_frame.tolerance, expected_tolerance);
1592            assert_close(feature_control.offset.x, expected_x);
1593            assert_close(feature_control.offset.y, expected_y);
1594            assert_close(
1595                f64::from(feature_control.font_scale),
1596                gdt_font_scale_for_height_mm(50.8).into(),
1597            );
1598        }
1599        Ok(())
1600    }
1601
1602    #[tokio::test(flavor = "multi_thread")]
1603    async fn gdt_distance_sets_units() -> Result<(), KclError> {
1604        let cases = [
1605            (
1606                "in",
1607                "2.54mm",
1608                "[10, -10]",
1609                kcmc::units::UnitLength::Inches,
1610                0.1,
1611                254.0,
1612                -254.0,
1613            ),
1614            (
1615                "cm",
1616                "10mm",
1617                "[1, -1]",
1618                kcmc::units::UnitLength::Centimeters,
1619                1.0,
1620                10.0,
1621                -10.0,
1622            ),
1623            (
1624                "mm",
1625                "2.54mm",
1626                "[10, -10]",
1627                kcmc::units::UnitLength::Millimeters,
1628                2.54,
1629                10.0,
1630                -10.0,
1631            ),
1632        ];
1633
1634        for (default_unit, tolerance, frame_position, scene_unit, expected_tolerance, expected_x, expected_y) in cases {
1635            let code = gdt_distance_kcl(default_unit, tolerance, frame_position);
1636            let commands = gdt_commands(&code).await;
1637            let annotation_index = new_annotation_command_index(&commands)?;
1638            let options = annotation_options(&commands[annotation_index])?;
1639
1640            assert_eq!(options.units, Some(scene_unit));
1641
1642            let dimension = options
1643                .dimension
1644                .as_ref()
1645                .expect("expected new_annotation command to have a dimension");
1646            assert_close(dimension.dimension.tolerance, expected_tolerance);
1647            assert_close(dimension.offset.x, expected_x);
1648            assert_close(dimension.offset.y, expected_y);
1649            assert_close(
1650                f64::from(dimension.font_scale),
1651                gdt_font_scale_for_height_mm(50.8).into(),
1652            );
1653        }
1654        Ok(())
1655    }
1656
1657    const GDT_DATUM_KCL: &str = r#"
1658blockProfile = sketch(on = XY) {
1659  edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
1660  edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
1661  edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
1662  edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
1663  coincident([edge1.end, edge2.start])
1664  coincident([edge2.end, edge3.start])
1665  coincident([edge3.end, edge4.start])
1666  coincident([edge4.end, edge1.start])
1667  horizontal(edge1)
1668  vertical(edge2)
1669  horizontal(edge3)
1670  vertical(edge4)
1671}
1672
1673block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
1674
1675gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
1676"#;
1677
1678    async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
1679        let settings = ExecutorSettings {
1680            skip_artifact_graph,
1681            ..Default::default()
1682        };
1683        let ctx = ExecutorContext::new_mock(Some(settings)).await;
1684        let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
1685        let mock_config = MockConfig {
1686            use_prev_memory: false,
1687            ..Default::default()
1688        };
1689        let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
1690        ctx.close().await;
1691
1692        outcome
1693            .artifact_graph
1694            .values()
1695            .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
1696            .count()
1697    }
1698
1699    #[tokio::test(flavor = "multi_thread")]
1700    async fn gdt_annotations_do_not_follow_runtime_artifact_graph_setting() {
1701        assert_eq!(gdt_artifact_count(false).await, 1);
1702        assert_eq!(gdt_artifact_count(true).await, 1);
1703    }
1704
1705    const GDT_ANGULARITY_FACE_KCL: &str = r#"
1706@settings(defaultLengthUnit = mm, kclVersion = 2)
1707
1708basicAngle = 30deg
1709thickness = 3.5mm
1710flangeLength = 24mm
1711bendStartX = 5mm
1712legLength = 30mm
1713legRun = legLength * cos(basicAngle)
1714legRise = legLength * sin(basicAngle)
1715normalRun = thickness * sin(basicAngle)
1716normalRise = thickness * cos(basicAngle)
1717annotationFont = 2mm
1718
1719stampedProfile = sketch(on = XY) {
1720  datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
1721  flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
1722  innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
1723  controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
1724  tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
1725  outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
1726  outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
1727  coincident([datumFace.end, flangeEnd.start])
1728  coincident([flangeEnd.end, innerFlange.start])
1729  coincident([innerFlange.end, controlledSurface.start])
1730  coincident([controlledSurface.end, tabEnd.start])
1731  coincident([tabEnd.end, outerSurface.start])
1732  coincident([outerSurface.end, outsideBend.start])
1733  coincident([outsideBend.end, datumFace.start])
1734  coincident([datumFace.start, ORIGIN])
1735  horizontal(datumFace)
1736  horizontal(innerFlange)
1737  vertical(flangeEnd)
1738  distance([datumFace.start, datumFace.end]) == flangeLength
1739  distance([flangeEnd.start, flangeEnd.end]) == thickness
1740  distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
1741  distance([controlledSurface.start, controlledSurface.end]) == legLength
1742  distance([tabEnd.start, tabEnd.end]) == thickness
1743  distance([outerSurface.start, outerSurface.end]) == legLength
1744  parallel([controlledSurface, outerSurface])
1745  perpendicular([controlledSurface, tabEnd])
1746  angle([datumFace, controlledSurface]) == basicAngle
1747}
1748
1749stampedPart = extrude(region(point = [12mm, 2mm], sketch = stampedProfile), length = 0.8mm)
1750
1751gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
1752gdt::angularity(faces = [stampedPart.sketch.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
1753"#;
1754
1755    const GDT_ANGULARITY_EDGE_KCL: &str = r#"
1756@settings(defaultLengthUnit = mm, kclVersion = 2)
1757
1758basicAngle = 30deg
1759thickness = 3.5mm
1760flangeLength = 24mm
1761bendStartX = 5mm
1762legLength = 30mm
1763legRun = legLength * cos(basicAngle)
1764legRise = legLength * sin(basicAngle)
1765normalRun = thickness * sin(basicAngle)
1766normalRise = thickness * cos(basicAngle)
1767annotationFont = 2mm
1768
1769stampedProfile = sketch(on = XY) {
1770  datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
1771  flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
1772  innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
1773  controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
1774  tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
1775  outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
1776  outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
1777  coincident([datumFace.end, flangeEnd.start])
1778  coincident([flangeEnd.end, innerFlange.start])
1779  coincident([innerFlange.end, controlledSurface.start])
1780  coincident([controlledSurface.end, tabEnd.start])
1781  coincident([tabEnd.end, outerSurface.start])
1782  coincident([outerSurface.end, outsideBend.start])
1783  coincident([outsideBend.end, datumFace.start])
1784  coincident([datumFace.start, ORIGIN])
1785  horizontal(datumFace)
1786  horizontal(innerFlange)
1787  vertical(flangeEnd)
1788  distance([datumFace.start, datumFace.end]) == flangeLength
1789  distance([flangeEnd.start, flangeEnd.end]) == thickness
1790  distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
1791  distance([controlledSurface.start, controlledSurface.end]) == legLength
1792  distance([tabEnd.start, tabEnd.end]) == thickness
1793  distance([outerSurface.start, outerSurface.end]) == legLength
1794  parallel([controlledSurface, outerSurface])
1795  perpendicular([controlledSurface, tabEnd])
1796  angle([datumFace, controlledSurface]) == basicAngle
1797}
1798
1799stampedRegion = region(point = [12mm, 2mm], sketch = stampedProfile)
1800hide(stampedProfile)
1801stampedPart = extrude(stampedRegion, length = 0.8mm)
1802
1803gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
1804gdt::angularity(edges = [stampedRegion.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
1805"#;
1806
1807    #[tokio::test(flavor = "multi_thread")]
1808    async fn gdt_angularity_uses_angularity_symbol_with_datums() -> Result<(), KclError> {
1809        let cases = [
1810            ("angled face", GDT_ANGULARITY_FACE_KCL, 0.1),
1811            ("angled edge", GDT_ANGULARITY_EDGE_KCL, 0.1),
1812        ];
1813
1814        for (label, code, expected_tolerance) in cases {
1815            let commands = gdt_commands(code).await;
1816            let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Angularity)?;
1817
1818            assert_close(control_frame.tolerance, expected_tolerance);
1819            assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
1820            assert!(control_frame.secondary_datum.is_none(), "case: {label}");
1821            assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
1822        }
1823        Ok(())
1824    }
1825
1826    // Mirrors the gdt::circularity doc examples: annotate a cylinder's circular
1827    // edge and its curved wall. Runs in mock mode, so it validates parsing, name
1828    // resolution, and that the control frame uses the Roundness (circularity)
1829    // symbol without datums. The doc examples additionally render against the
1830    // engine in kcl_test_examples.
1831    const GDT_CIRCULARITY_EDGE_KCL: &str = r#"
1832@settings(defaultLengthUnit = mm, kclVersion = 2)
1833
1834cylinderSketch = sketch(on = XY) {
1835  perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1836}
1837
1838cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
1839hide(cylinderSketch)
1840cylinder = extrude(cylinderRegion, length = 10mm)
1841gdt::circularity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm)
1842"#;
1843
1844    const GDT_CIRCULARITY_WALL_KCL: &str = r#"
1845@settings(defaultLengthUnit = mm, kclVersion = 2)
1846
1847cylinderSketch = sketch(on = XY) {
1848  perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1849}
1850
1851cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
1852gdt::circularity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [12mm, 8mm], framePlane = XZ)
1853"#;
1854
1855    const GDT_CIRCULARITY_COMMON_EDGE_KCL: &str = r#"
1856@settings(defaultLengthUnit = mm, kclVersion = 2)
1857
1858cylinderSketch = sketch(on = XY) {
1859  perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1860}
1861
1862cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
1863topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
1864gdt::circularity(edges = [topEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
1865"#;
1866
1867    #[tokio::test(flavor = "multi_thread")]
1868    async fn gdt_circularity_uses_roundness_symbol_without_datums() -> Result<(), KclError> {
1869        let cases = [
1870            ("circular edge", GDT_CIRCULARITY_EDGE_KCL, 0.05),
1871            ("cylinder wall", GDT_CIRCULARITY_WALL_KCL, 0.02),
1872            ("common edge", GDT_CIRCULARITY_COMMON_EDGE_KCL, 0.05),
1873        ];
1874
1875        for (label, code, expected_tolerance) in cases {
1876            let commands = gdt_commands(code).await;
1877            let annotation_index = new_annotation_command_index(&commands)?;
1878            let feature_control = feature_control(&commands[annotation_index])?;
1879            let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1880                KclError::new_internal(KclErrorDetails::new(
1881                    format!("expected {label} feature_control to have a control_frame"),
1882                    vec![SourceRange::default()],
1883                ))
1884            })?;
1885
1886            assert_eq!(control_frame.symbol, MbdSymbol::Roundness, "case: {label}");
1887            assert_close(control_frame.tolerance, expected_tolerance);
1888            // Circularity is a form tolerance and never references datums.
1889            assert!(control_frame.primary_datum.is_none(), "case: {label}");
1890            assert!(control_frame.secondary_datum.is_none(), "case: {label}");
1891            assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
1892        }
1893        Ok(())
1894    }
1895
1896    // Mirrors the gdt::cylindricity doc examples: annotate a cylinder's curved
1897    // wall and its circular edge. Runs in mock mode, so it validates parsing,
1898    // name resolution, and that the control frame uses the Cylindricity symbol
1899    // without datums. The doc examples additionally render against the engine in
1900    // kcl_test_examples.
1901    const GDT_CYLINDRICITY_WALL_KCL: &str = r#"
1902@settings(defaultLengthUnit = mm, kclVersion = 2)
1903
1904cylinderSketch = sketch(on = XY) {
1905  perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1906}
1907
1908cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
1909gdt::cylindricity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [-12mm, 8mm], framePlane = XZ)
1910"#;
1911
1912    const GDT_CYLINDRICITY_EDGE_KCL: &str = r#"
1913@settings(defaultLengthUnit = mm, kclVersion = 2)
1914
1915cylinderSketch = sketch(on = XY) {
1916  perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1917}
1918
1919cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
1920hide(cylinderSketch)
1921cylinder = extrude(cylinderRegion, length = 10mm)
1922gdt::cylindricity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm, framePosition = [-12mm, 8mm])
1923"#;
1924
1925    const GDT_CYLINDRICITY_COMMON_EDGE_KCL: &str = r#"
1926@settings(defaultLengthUnit = mm, kclVersion = 2)
1927
1928cylinderSketch = sketch(on = XY) {
1929  perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1930}
1931
1932cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
1933topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
1934gdt::cylindricity(edges = [topEdge], tolerance = 0.05mm, framePosition = [-12mm, 8mm], framePlane = XZ)
1935"#;
1936
1937    #[tokio::test(flavor = "multi_thread")]
1938    async fn gdt_cylindricity_uses_cylindricity_symbol_without_datums() -> Result<(), KclError> {
1939        let cases = [
1940            ("cylinder wall", GDT_CYLINDRICITY_WALL_KCL, 0.02),
1941            ("circular edge", GDT_CYLINDRICITY_EDGE_KCL, 0.05),
1942            ("common edge", GDT_CYLINDRICITY_COMMON_EDGE_KCL, 0.05),
1943        ];
1944
1945        for (label, code, expected_tolerance) in cases {
1946            let commands = gdt_commands(code).await;
1947            let annotation_index = new_annotation_command_index(&commands)?;
1948            let feature_control = feature_control(&commands[annotation_index])?;
1949            let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1950                KclError::new_internal(KclErrorDetails::new(
1951                    format!("expected {label} feature_control to have a control_frame"),
1952                    vec![SourceRange::default()],
1953                ))
1954            })?;
1955
1956            assert_eq!(control_frame.symbol, MbdSymbol::Cylindricity, "case: {label}");
1957            assert_close(control_frame.tolerance, expected_tolerance);
1958            // Cylindricity is a form tolerance and never references datums.
1959            assert!(control_frame.primary_datum.is_none(), "case: {label}");
1960            assert!(control_frame.secondary_datum.is_none(), "case: {label}");
1961            assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
1962        }
1963        Ok(())
1964    }
1965}