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