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;
47
48// Calibration target: measured annotation text/frame height in millimeters when
49// font_scale is 1.0 and GDT_FONT_TEXTURE_POINT_SIZE is fixed. Tune this value from
50// scene measurements, not by exposing engine font_point_size to users.
51const GDT_FONT_SCALE_1_HEIGHT_MM: f64 = 8.0;
52
53fn gdt_font_scale(font_size: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
54    let requested_height_mm = font_size.map(TyF64::to_mm).unwrap_or(DEFAULT_GDT_FONT_SIZE_MM);
55    if requested_height_mm <= 0.0 {
56        return Err(KclError::new_semantic(KclErrorDetails::new(
57            "fontSize must be greater than 0.".to_owned(),
58            vec![args.source_range],
59        )));
60    }
61    Ok(gdt_font_scale_for_height_mm(requested_height_mm))
62}
63
64fn gdt_font_scale_for_height_mm(requested_height_mm: f64) -> f32 {
65    (requested_height_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32
66}
67
68#[derive(Debug, Clone)]
69enum DistanceEntity {
70    Face(Box<Face>),
71    TaggedFace(Box<TagIdentifier>),
72    Edge(EdgeReference),
73}
74
75#[derive(Debug, Clone, Copy)]
76struct DistanceEndpoint {
77    entity_id: uuid::Uuid,
78    entity_pos: KPoint2d<f64>,
79}
80
81fn add_gdt_annotation_artifact(exec_state: &mut ExecState, args: &Args, annotation_id: uuid::Uuid) {
82    if args.ctx.settings.skip_artifact_graph {
83        return;
84    }
85
86    exec_state.add_artifact(Artifact::GdtAnnotation(GdtAnnotationArtifact {
87        id: ArtifactId::new(annotation_id),
88        code_ref: CodeRef::placeholder(args.source_range),
89    }));
90}
91
92impl DistanceEntity {
93    async fn to_endpoint(&self, exec_state: &mut ExecState, args: &Args) -> Result<DistanceEndpoint, KclError> {
94        match self {
95            DistanceEntity::Face(face) => Ok(DistanceEndpoint {
96                entity_id: face.id,
97                entity_pos: KPoint2d { x: 0.5, y: 0.5 },
98            }),
99            DistanceEntity::TaggedFace(face) => Ok(DistanceEndpoint {
100                entity_id: args.get_adjacent_face_to_tag(exec_state, face, false).await?,
101                entity_pos: KPoint2d { x: 0.5, y: 0.5 },
102            }),
103            DistanceEntity::Edge(edge) => Ok(DistanceEndpoint {
104                entity_id: edge.get_engine_id(exec_state, args)?,
105                entity_pos: KPoint2d { x: 0.5, y: 0.0 },
106            }),
107        }
108    }
109}
110
111impl<'a> FromKclValue<'a> for DistanceEntity {
112    fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
113        match arg {
114            KclValue::Face { value } => Some(Self::Face(value.to_owned())),
115            KclValue::Uuid { value, .. } => Some(Self::Edge(EdgeReference::Uuid(*value))),
116            KclValue::TagIdentifier(value) => Some(Self::TaggedFace(value.to_owned())),
117            _ => None,
118        }
119    }
120}
121
122fn distance_entity_type() -> RuntimeType {
123    RuntimeType::Union(vec![
124        RuntimeType::face(),
125        RuntimeType::tagged_face(),
126        RuntimeType::edge(),
127    ])
128}
129
130pub async fn datum(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
131    let face: TagIdentifier = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
132    let name: String = args.get_kw_arg("name", &RuntimeType::string(), exec_state)?;
133    let frame_position: Option<[TyF64; 2]> =
134        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
135    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
136    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
137    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
138
139    let annotation = inner_datum(
140        face,
141        name,
142        frame_position,
143        frame_plane,
144        leader_scale,
145        font_size,
146        exec_state,
147        &args,
148    )
149    .await?;
150    Ok(KclValue::GdtAnnotation {
151        value: Box::new(annotation),
152    })
153}
154
155#[allow(clippy::too_many_arguments)]
156async fn inner_datum(
157    face: TagIdentifier,
158    name: String,
159    frame_position: Option<[TyF64; 2]>,
160    frame_plane: Option<Plane>,
161    leader_scale: Option<TyF64>,
162    font_size: Option<TyF64>,
163    exec_state: &mut ExecState,
164    args: &Args,
165) -> Result<GdtAnnotation, KclError> {
166    const DATUM_LENGTH_ERROR: &str = "Datum name must be a single character.";
167    if name.len() > 1 {
168        return Err(KclError::new_semantic(KclErrorDetails::new(
169            DATUM_LENGTH_ERROR.to_owned(),
170            vec![args.source_range],
171        )));
172    }
173    let name_char = name.chars().next().ok_or_else(|| {
174        KclError::new_semantic(KclErrorDetails::new(
175            DATUM_LENGTH_ERROR.to_owned(),
176            vec![args.source_range],
177        ))
178    })?;
179    let mut frame_plane = if let Some(plane) = frame_plane {
180        plane
181    } else {
182        // No plane given. Use one of the standard planes.
183        xy_plane(exec_state, args).await?
184    };
185    ensure_sketch_plane_in_engine(
186        &mut frame_plane,
187        exec_state,
188        &args.ctx,
189        args.source_range,
190        args.node_path.clone(),
191    )
192    .await?;
193    let face_id = args.get_adjacent_face_to_tag(exec_state, &face, false).await?;
194    let meta = vec![Metadata::from(args.source_range)];
195    let annotation_id = exec_state.next_uuid();
196    let feature_control = AnnotationFeatureControl::builder()
197        .entity_id(face_id)
198        // Point to the center of the face.
199        .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
200        .leader_type(AnnotationLineEnd::Dot)
201        .defined_datum(name_char)
202        .plane_id(frame_plane.id)
203        .offset(if let Some(offset) = &frame_position {
204            KPoint2d {
205                x: offset[0].to_mm(),
206                y: offset[1].to_mm(),
207            }
208        } else {
209            KPoint2d { x: 100.0, y: 100.0 }
210        })
211        .precision(0)
212        .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
213        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
214        .leader_scale(leader_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0))
215        .build();
216    exec_state
217        .batch_modeling_cmd(
218            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
219            ModelingCmd::from(
220                mcmd::NewAnnotation::builder()
221                    .options(AnnotationOptions::builder().feature_control(feature_control).build())
222                    .clobber(false)
223                    .annotation_type(AnnotationType::T3D)
224                    .build(),
225            ),
226        )
227        .await?;
228    add_gdt_annotation_artifact(exec_state, args, annotation_id);
229    Ok(GdtAnnotation {
230        id: annotation_id,
231        meta,
232    })
233}
234
235pub async fn flatness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
236    let faces: Vec<TagIdentifier> = args.get_kw_arg(
237        "faces",
238        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
239        exec_state,
240    )?;
241    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
242    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
243    let frame_position: Option<[TyF64; 2]> =
244        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
245    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
246    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
247    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
248
249    let annotations = inner_flatness(
250        faces,
251        tolerance,
252        precision,
253        frame_position,
254        frame_plane,
255        leader_scale,
256        font_size,
257        exec_state,
258        &args,
259    )
260    .await?;
261    Ok(annotations.into())
262}
263
264pub async fn profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
265    let edges: Vec<EdgeReference> = args.get_kw_arg(
266        "edges",
267        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
268        exec_state,
269    )?;
270    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
271        "datums",
272        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
273        exec_state,
274    )?;
275    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
276    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
277    let frame_position: Option<[TyF64; 2]> =
278        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
279    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
280    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
281    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
282
283    let annotations = inner_profile(
284        edges,
285        datums,
286        tolerance,
287        precision,
288        frame_position,
289        frame_plane,
290        leader_scale,
291        font_size,
292        exec_state,
293        &args,
294    )
295    .await?;
296    Ok(annotations.into())
297}
298
299pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
300    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
301        "faces",
302        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
303        exec_state,
304    )?;
305    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
306        "edges",
307        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
308        exec_state,
309    )?;
310    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
311        "datums",
312        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
313        exec_state,
314    )?;
315    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
316    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
317    let frame_position: Option<[TyF64; 2]> =
318        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
319    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
320    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
321    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
322
323    let annotations = inner_position(
324        faces.unwrap_or_default(),
325        edges.unwrap_or_default(),
326        tolerance,
327        datums,
328        precision,
329        frame_position,
330        frame_plane,
331        leader_scale,
332        font_size,
333        exec_state,
334        &args,
335    )
336    .await?;
337    Ok(annotations.into())
338}
339
340pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
341    let from: Option<DistanceEntity> = args.get_kw_arg_opt("from", &distance_entity_type(), exec_state)?;
342    let to: Option<DistanceEntity> = args.get_kw_arg_opt("to", &distance_entity_type(), exec_state)?;
343    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
344        "edges",
345        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
346        exec_state,
347    )?;
348    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
349    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
350    let frame_position: Option<[TyF64; 2]> =
351        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
352    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
353    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
354    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
355
356    let annotations = inner_distance(
357        from,
358        to,
359        edges.unwrap_or_default(),
360        tolerance,
361        precision,
362        frame_position,
363        frame_plane,
364        leader_scale,
365        font_size,
366        exec_state,
367        &args,
368    )
369    .await?;
370    Ok(annotations.into())
371}
372
373pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
374    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
375        "faces",
376        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
377        exec_state,
378    )?;
379    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
380        "edges",
381        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
382        exec_state,
383    )?;
384    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
385        "datums",
386        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
387        exec_state,
388    )?;
389    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
390    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
391    let frame_position: Option<[TyF64; 2]> =
392        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
393    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
394    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
395    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
396
397    let annotations = inner_perpendicularity(
398        faces.unwrap_or_default(),
399        edges.unwrap_or_default(),
400        datums,
401        tolerance,
402        precision,
403        frame_position,
404        frame_plane,
405        leader_scale,
406        font_size,
407        exec_state,
408        &args,
409    )
410    .await?;
411    Ok(annotations.into())
412}
413
414pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
415    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
416        "faces",
417        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
418        exec_state,
419    )?;
420    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
421        "edges",
422        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
423        exec_state,
424    )?;
425    let datums: Option<Vec<String>> = args.get_kw_arg_opt(
426        "datums",
427        &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
428        exec_state,
429    )?;
430    let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
431    let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
432    let frame_position: Option<[TyF64; 2]> =
433        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
434    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
435    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
436    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
437
438    let annotations = inner_parallelism(
439        faces.unwrap_or_default(),
440        edges.unwrap_or_default(),
441        datums,
442        tolerance,
443        precision,
444        frame_position,
445        frame_plane,
446        leader_scale,
447        font_size,
448        exec_state,
449        &args,
450    )
451    .await?;
452    Ok(annotations.into())
453}
454
455pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
456    let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
457    let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
458        "faces",
459        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
460        exec_state,
461    )?;
462    let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
463        "edges",
464        &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
465        exec_state,
466    )?;
467    let frame_position: Option<[TyF64; 2]> =
468        args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
469    let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
470    let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
471    let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
472
473    let annotations = inner_annotation(
474        annotation,
475        faces.unwrap_or_default(),
476        edges.unwrap_or_default(),
477        frame_position,
478        frame_plane,
479        leader_scale,
480        font_size,
481        exec_state,
482        &args,
483    )
484    .await?;
485    Ok(annotations.into())
486}
487
488#[allow(clippy::too_many_arguments)]
489async fn inner_perpendicularity(
490    faces: Vec<TagIdentifier>,
491    edges: Vec<EdgeReference>,
492    datums: Option<Vec<String>>,
493    tolerance: TyF64,
494    precision: Option<TyF64>,
495    frame_position: Option<[TyF64; 2]>,
496    frame_plane: Option<Plane>,
497    leader_scale: Option<TyF64>,
498    font_size: Option<TyF64>,
499    exec_state: &mut ExecState,
500    args: &Args,
501) -> Result<Vec<GdtAnnotation>, KclError> {
502    if faces.is_empty() && edges.is_empty() {
503        return Err(KclError::new_semantic(KclErrorDetails::new(
504            "Perpendicularity requires at least one face or edge.".to_owned(),
505            vec![args.source_range],
506        )));
507    }
508
509    let precision = resolve_precision(precision, args)?;
510    let datums = resolve_datums(datums, args, "Perpendicularity")?;
511    let mut frame_plane = if let Some(plane) = frame_plane {
512        plane
513    } else {
514        xy_plane(exec_state, args).await?
515    };
516    ensure_sketch_plane_in_engine(
517        &mut frame_plane,
518        exec_state,
519        &args.ctx,
520        args.source_range,
521        args.node_path.clone(),
522    )
523    .await?;
524
525    let mut annotations = Vec::with_capacity(faces.len() + edges.len());
526    for face in &faces {
527        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
528        create_feature_control_annotation(
529            face_id,
530            MbdSymbol::Perpendicularity,
531            &tolerance,
532            &datums,
533            precision,
534            frame_position.as_ref(),
535            frame_plane.id,
536            leader_scale.as_ref(),
537            font_size.as_ref(),
538            exec_state,
539            args,
540            &mut annotations,
541        )
542        .await?;
543    }
544    for edge in &edges {
545        let edge_id = edge.get_engine_id(exec_state, args)?;
546        create_feature_control_annotation(
547            edge_id,
548            MbdSymbol::Perpendicularity,
549            &tolerance,
550            &datums,
551            precision,
552            frame_position.as_ref(),
553            frame_plane.id,
554            leader_scale.as_ref(),
555            font_size.as_ref(),
556            exec_state,
557            args,
558            &mut annotations,
559        )
560        .await?;
561    }
562
563    Ok(annotations)
564}
565
566#[allow(clippy::too_many_arguments)]
567async fn inner_parallelism(
568    faces: Vec<TagIdentifier>,
569    edges: Vec<EdgeReference>,
570    datums: Option<Vec<String>>,
571    tolerance: TyF64,
572    precision: Option<TyF64>,
573    frame_position: Option<[TyF64; 2]>,
574    frame_plane: Option<Plane>,
575    leader_scale: Option<TyF64>,
576    font_size: Option<TyF64>,
577    exec_state: &mut ExecState,
578    args: &Args,
579) -> Result<Vec<GdtAnnotation>, KclError> {
580    if faces.is_empty() && edges.is_empty() {
581        return Err(KclError::new_semantic(KclErrorDetails::new(
582            "Parallelism requires at least one face or edge.".to_owned(),
583            vec![args.source_range],
584        )));
585    }
586
587    let precision = resolve_precision(precision, args)?;
588    let datums = resolve_datums(datums, args, "Parallelism")?;
589    let mut frame_plane = if let Some(plane) = frame_plane {
590        plane
591    } else {
592        xy_plane(exec_state, args).await?
593    };
594    ensure_sketch_plane_in_engine(
595        &mut frame_plane,
596        exec_state,
597        &args.ctx,
598        args.source_range,
599        args.node_path.clone(),
600    )
601    .await?;
602
603    let mut annotations = Vec::with_capacity(faces.len() + edges.len());
604    for face in &faces {
605        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
606        create_feature_control_annotation(
607            face_id,
608            MbdSymbol::Parallelism,
609            &tolerance,
610            &datums,
611            precision,
612            frame_position.as_ref(),
613            frame_plane.id,
614            leader_scale.as_ref(),
615            font_size.as_ref(),
616            exec_state,
617            args,
618            &mut annotations,
619        )
620        .await?;
621    }
622    for edge in &edges {
623        let edge_id = edge.get_engine_id(exec_state, args)?;
624        create_feature_control_annotation(
625            edge_id,
626            MbdSymbol::Parallelism,
627            &tolerance,
628            &datums,
629            precision,
630            frame_position.as_ref(),
631            frame_plane.id,
632            leader_scale.as_ref(),
633            font_size.as_ref(),
634            exec_state,
635            args,
636            &mut annotations,
637        )
638        .await?;
639    }
640
641    Ok(annotations)
642}
643
644#[allow(clippy::too_many_arguments)]
645async fn inner_annotation(
646    annotation: String,
647    faces: Vec<TagIdentifier>,
648    edges: Vec<EdgeReference>,
649    frame_position: Option<[TyF64; 2]>,
650    frame_plane: Option<Plane>,
651    leader_scale: Option<TyF64>,
652    font_size: Option<TyF64>,
653    exec_state: &mut ExecState,
654    args: &Args,
655) -> Result<Vec<GdtAnnotation>, KclError> {
656    if annotation.is_empty() {
657        return Err(KclError::new_semantic(KclErrorDetails::new(
658            "Annotation text must not be empty.".to_owned(),
659            vec![args.source_range],
660        )));
661    }
662    if faces.is_empty() && edges.is_empty() {
663        return Err(KclError::new_semantic(KclErrorDetails::new(
664            "Annotation requires at least one face or edge.".to_owned(),
665            vec![args.source_range],
666        )));
667    }
668
669    let mut frame_plane = if let Some(plane) = frame_plane {
670        plane
671    } else {
672        xy_plane(exec_state, args).await?
673    };
674    ensure_sketch_plane_in_engine(
675        &mut frame_plane,
676        exec_state,
677        &args.ctx,
678        args.source_range,
679        args.node_path.clone(),
680    )
681    .await?;
682
683    let mut annotations = Vec::with_capacity(faces.len() + edges.len());
684    for face in &faces {
685        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
686        create_annotation(
687            face_id,
688            &annotation,
689            frame_position.as_ref(),
690            frame_plane.id,
691            leader_scale.as_ref(),
692            font_size.as_ref(),
693            exec_state,
694            args,
695            &mut annotations,
696        )
697        .await?;
698    }
699    for edge in &edges {
700        let edge_id = edge.get_engine_id(exec_state, args)?;
701        create_annotation(
702            edge_id,
703            &annotation,
704            frame_position.as_ref(),
705            frame_plane.id,
706            leader_scale.as_ref(),
707            font_size.as_ref(),
708            exec_state,
709            args,
710            &mut annotations,
711        )
712        .await?;
713    }
714
715    Ok(annotations)
716}
717
718#[allow(clippy::too_many_arguments)]
719async fn inner_distance(
720    from: Option<DistanceEntity>,
721    to: Option<DistanceEntity>,
722    edges: Vec<EdgeReference>,
723    tolerance: TyF64,
724    precision: Option<TyF64>,
725    frame_position: Option<[TyF64; 2]>,
726    frame_plane: Option<Plane>,
727    leader_scale: Option<TyF64>,
728    font_size: Option<TyF64>,
729    exec_state: &mut ExecState,
730    args: &Args,
731) -> Result<Vec<GdtAnnotation>, KclError> {
732    let precision = resolve_precision(precision, args)?;
733    let mut frame_plane = if let Some(plane) = frame_plane {
734        plane
735    } else {
736        xy_plane(exec_state, args).await?
737    };
738    ensure_sketch_plane_in_engine(
739        &mut frame_plane,
740        exec_state,
741        &args.ctx,
742        args.source_range,
743        args.node_path.clone(),
744    )
745    .await?;
746
747    if from.is_some() || to.is_some() {
748        if !edges.is_empty() {
749            return Err(KclError::new_semantic(KclErrorDetails::new(
750                "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
751                vec![args.source_range],
752            )));
753        }
754
755        let (Some(from), Some(to)) = (from, to) else {
756            return Err(KclError::new_semantic(KclErrorDetails::new(
757                "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
758                vec![args.source_range],
759            )));
760        };
761
762        let from = from.to_endpoint(exec_state, args).await?;
763        let to = to.to_endpoint(exec_state, args).await?;
764        let mut annotations = Vec::with_capacity(1);
765        create_basic_distance_annotation(
766            from,
767            to,
768            &tolerance,
769            precision,
770            frame_position.as_ref(),
771            frame_plane.id,
772            leader_scale.as_ref(),
773            font_size.as_ref(),
774            exec_state,
775            args,
776            &mut annotations,
777        )
778        .await?;
779        return Ok(annotations);
780    }
781
782    if edges.is_empty() {
783        return Err(KclError::new_semantic(KclErrorDetails::new(
784            "Distance requires either `edges` or both `from` and `to`.".to_owned(),
785            vec![args.source_range],
786        )));
787    }
788
789    let mut annotations = Vec::with_capacity(edges.len());
790    for edge in &edges {
791        let edge_id = edge.get_engine_id(exec_state, args)?;
792        create_basic_distance_annotation(
793            DistanceEndpoint {
794                entity_id: edge_id,
795                entity_pos: KPoint2d { x: 0.0, y: 0.0 },
796            },
797            DistanceEndpoint {
798                entity_id: edge_id,
799                entity_pos: KPoint2d { x: 1.0, y: 0.0 },
800            },
801            &tolerance,
802            precision,
803            frame_position.as_ref(),
804            frame_plane.id,
805            leader_scale.as_ref(),
806            font_size.as_ref(),
807            exec_state,
808            args,
809            &mut annotations,
810        )
811        .await?;
812    }
813    Ok(annotations)
814}
815
816#[allow(clippy::too_many_arguments)]
817async fn inner_profile(
818    edges: Vec<EdgeReference>,
819    datums: Option<Vec<String>>,
820    tolerance: TyF64,
821    precision: Option<TyF64>,
822    frame_position: Option<[TyF64; 2]>,
823    frame_plane: Option<Plane>,
824    leader_scale: Option<TyF64>,
825    font_size: Option<TyF64>,
826    exec_state: &mut ExecState,
827    args: &Args,
828) -> Result<Vec<GdtAnnotation>, KclError> {
829    let precision = resolve_precision(precision, args)?;
830    let datums = resolve_datums(datums, args, "Profile")?;
831    let mut frame_plane = if let Some(plane) = frame_plane {
832        plane
833    } else {
834        xy_plane(exec_state, args).await?
835    };
836    ensure_sketch_plane_in_engine(
837        &mut frame_plane,
838        exec_state,
839        &args.ctx,
840        args.source_range,
841        args.node_path.clone(),
842    )
843    .await?;
844
845    let mut annotations = Vec::with_capacity(edges.len());
846    for edge in &edges {
847        let edge_id = edge.get_engine_id(exec_state, args)?;
848        create_feature_control_annotation(
849            edge_id,
850            MbdSymbol::ProfileOfLine,
851            &tolerance,
852            &datums,
853            precision,
854            frame_position.as_ref(),
855            frame_plane.id,
856            leader_scale.as_ref(),
857            font_size.as_ref(),
858            exec_state,
859            args,
860            &mut annotations,
861        )
862        .await?;
863    }
864    Ok(annotations)
865}
866
867#[allow(clippy::too_many_arguments)]
868async fn inner_position(
869    faces: Vec<TagIdentifier>,
870    edges: Vec<EdgeReference>,
871    tolerance: TyF64,
872    datums: Option<Vec<String>>,
873    precision: Option<TyF64>,
874    frame_position: Option<[TyF64; 2]>,
875    frame_plane: Option<Plane>,
876    leader_scale: Option<TyF64>,
877    font_size: Option<TyF64>,
878    exec_state: &mut ExecState,
879    args: &Args,
880) -> Result<Vec<GdtAnnotation>, KclError> {
881    if faces.is_empty() && edges.is_empty() {
882        return Err(KclError::new_semantic(KclErrorDetails::new(
883            "Position requires at least one face or edge.".to_owned(),
884            vec![args.source_range],
885        )));
886    }
887
888    let precision = resolve_precision(precision, args)?;
889    let datums = resolve_datums(datums, args, "Position")?;
890    let mut frame_plane = if let Some(plane) = frame_plane {
891        plane
892    } else {
893        xy_plane(exec_state, args).await?
894    };
895    ensure_sketch_plane_in_engine(
896        &mut frame_plane,
897        exec_state,
898        &args.ctx,
899        args.source_range,
900        args.node_path.clone(),
901    )
902    .await?;
903
904    let mut annotations = Vec::with_capacity(faces.len() + edges.len());
905    for face in &faces {
906        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
907        create_feature_control_annotation(
908            face_id,
909            MbdSymbol::Position,
910            &tolerance,
911            &datums,
912            precision,
913            frame_position.as_ref(),
914            frame_plane.id,
915            leader_scale.as_ref(),
916            font_size.as_ref(),
917            exec_state,
918            args,
919            &mut annotations,
920        )
921        .await?;
922    }
923    for edge in &edges {
924        let edge_id = edge.get_engine_id(exec_state, args)?;
925        create_feature_control_annotation(
926            edge_id,
927            MbdSymbol::Position,
928            &tolerance,
929            &datums,
930            precision,
931            frame_position.as_ref(),
932            frame_plane.id,
933            leader_scale.as_ref(),
934            font_size.as_ref(),
935            exec_state,
936            args,
937            &mut annotations,
938        )
939        .await?;
940    }
941    Ok(annotations)
942}
943
944#[allow(clippy::too_many_arguments)]
945async fn inner_flatness(
946    faces: Vec<TagIdentifier>,
947    tolerance: TyF64,
948    precision: Option<TyF64>,
949    frame_position: Option<[TyF64; 2]>,
950    frame_plane: Option<Plane>,
951    leader_scale: Option<TyF64>,
952    font_size: Option<TyF64>,
953    exec_state: &mut ExecState,
954    args: &Args,
955) -> Result<Vec<GdtAnnotation>, KclError> {
956    let precision = resolve_precision(precision, args)?;
957    let mut frame_plane = if let Some(plane) = frame_plane {
958        plane
959    } else {
960        // No plane given. Use one of the standard planes.
961        xy_plane(exec_state, args).await?
962    };
963    ensure_sketch_plane_in_engine(
964        &mut frame_plane,
965        exec_state,
966        &args.ctx,
967        args.source_range,
968        args.node_path.clone(),
969    )
970    .await?;
971    let mut annotations = Vec::with_capacity(faces.len());
972    for face in &faces {
973        let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
974        let meta = vec![Metadata::from(args.source_range)];
975        let annotation_id = exec_state.next_uuid();
976        let feature_control = AnnotationFeatureControl::builder()
977            .entity_id(face_id)
978            // Point to the center of the face.
979            .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
980            .leader_type(AnnotationLineEnd::Dot)
981            .control_frame(
982                AnnotationMbdControlFrame::builder()
983                    .symbol(MbdSymbol::Flatness)
984                    .tolerance(tolerance.to_mm())
985                    .build(),
986            )
987            .plane_id(frame_plane.id)
988            .offset(if let Some(offset) = &frame_position {
989                KPoint2d {
990                    x: offset[0].to_mm(),
991                    y: offset[1].to_mm(),
992                }
993            } else {
994                KPoint2d { x: 100.0, y: 100.0 }
995            })
996            .precision(precision)
997            .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
998            .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
999            .leader_scale(leader_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0))
1000            .build();
1001        let options = AnnotationOptions::builder().feature_control(feature_control).build();
1002        exec_state
1003            .batch_modeling_cmd(
1004                ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1005                ModelingCmd::from(
1006                    mcmd::NewAnnotation::builder()
1007                        .options(options)
1008                        .clobber(false)
1009                        .annotation_type(AnnotationType::T3D)
1010                        .build(),
1011                ),
1012            )
1013            .await?;
1014        add_gdt_annotation_artifact(exec_state, args, annotation_id);
1015        annotations.push(GdtAnnotation {
1016            id: annotation_id,
1017            meta,
1018        });
1019    }
1020    Ok(annotations)
1021}
1022
1023fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
1024    if let Some(precision) = precision {
1025        let rounded = precision.n.round();
1026        if !(0.0..=9.0).contains(&rounded) {
1027            return Err(KclError::new_semantic(KclErrorDetails::new(
1028                "Precision must be between 0 and 9".to_owned(),
1029                vec![args.source_range],
1030            )));
1031        }
1032        Ok(rounded as u32)
1033    } else {
1034        Ok(3)
1035    }
1036}
1037
1038#[allow(clippy::too_many_arguments)]
1039async fn create_basic_distance_annotation(
1040    from: DistanceEndpoint,
1041    to: DistanceEndpoint,
1042    tolerance: &TyF64,
1043    precision: u32,
1044    frame_position: Option<&[TyF64; 2]>,
1045    frame_plane_id: uuid::Uuid,
1046    leader_scale: Option<&TyF64>,
1047    font_size: Option<&TyF64>,
1048    exec_state: &mut ExecState,
1049    args: &Args,
1050    annotations: &mut Vec<GdtAnnotation>,
1051) -> Result<(), KclError> {
1052    let meta = vec![Metadata::from(args.source_range)];
1053    let annotation_id = exec_state.next_uuid();
1054    let dimension = AnnotationBasicDimension::builder()
1055        .from_entity_id(from.entity_id)
1056        .from_entity_pos(from.entity_pos)
1057        .to_entity_id(to.entity_id)
1058        .to_entity_pos(to.entity_pos)
1059        .dimension(
1060            AnnotationMbdBasicDimension::builder()
1061                .tolerance(tolerance.to_mm())
1062                .build(),
1063        )
1064        .plane_id(frame_plane_id)
1065        .offset(if let Some(offset) = frame_position {
1066            KPoint2d {
1067                x: offset[0].to_mm(),
1068                y: offset[1].to_mm(),
1069            }
1070        } else {
1071            KPoint2d { x: 100.0, y: 100.0 }
1072        })
1073        .precision(precision)
1074        .font_scale(gdt_font_scale(font_size, args)?)
1075        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1076        .arrow_scale(leader_scale.map(|n| n.n as f32).unwrap_or(1.0))
1077        .build();
1078    let options = AnnotationOptions::builder().dimension(dimension).build();
1079    exec_state
1080        .batch_modeling_cmd(
1081            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1082            ModelingCmd::from(
1083                mcmd::NewAnnotation::builder()
1084                    .options(options)
1085                    .clobber(false)
1086                    .annotation_type(AnnotationType::T3D)
1087                    .build(),
1088            ),
1089        )
1090        .await?;
1091    add_gdt_annotation_artifact(exec_state, args, annotation_id);
1092    annotations.push(GdtAnnotation {
1093        id: annotation_id,
1094        meta,
1095    });
1096    Ok(())
1097}
1098
1099#[allow(clippy::too_many_arguments)]
1100async fn create_feature_control_annotation(
1101    entity_id: uuid::Uuid,
1102    symbol: MbdSymbol,
1103    tolerance: &TyF64,
1104    datums: &[char],
1105    precision: u32,
1106    frame_position: Option<&[TyF64; 2]>,
1107    frame_plane_id: uuid::Uuid,
1108    leader_scale: Option<&TyF64>,
1109    font_size: Option<&TyF64>,
1110    exec_state: &mut ExecState,
1111    args: &Args,
1112    annotations: &mut Vec<GdtAnnotation>,
1113) -> Result<(), KclError> {
1114    let meta = vec![Metadata::from(args.source_range)];
1115    let annotation_id = exec_state.next_uuid();
1116    let control_frame = gdt_control_frame(symbol, tolerance.to_mm(), datums);
1117    let feature_control = AnnotationFeatureControl::builder()
1118        .entity_id(entity_id)
1119        .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1120        .leader_type(AnnotationLineEnd::Dot)
1121        .control_frame(control_frame)
1122        .plane_id(frame_plane_id)
1123        .offset(if let Some(offset) = frame_position {
1124            KPoint2d {
1125                x: offset[0].to_mm(),
1126                y: offset[1].to_mm(),
1127            }
1128        } else {
1129            KPoint2d { x: 100.0, y: 100.0 }
1130        })
1131        .precision(precision)
1132        .font_scale(gdt_font_scale(font_size, args)?)
1133        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1134        .leader_scale(leader_scale.map(|n| n.n as f32).unwrap_or(1.0))
1135        .build();
1136    let options = AnnotationOptions::builder().feature_control(feature_control).build();
1137    exec_state
1138        .batch_modeling_cmd(
1139            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1140            ModelingCmd::from(
1141                mcmd::NewAnnotation::builder()
1142                    .options(options)
1143                    .clobber(false)
1144                    .annotation_type(AnnotationType::T3D)
1145                    .build(),
1146            ),
1147        )
1148        .await?;
1149    add_gdt_annotation_artifact(exec_state, args, annotation_id);
1150    annotations.push(GdtAnnotation {
1151        id: annotation_id,
1152        meta,
1153    });
1154    Ok(())
1155}
1156
1157#[allow(clippy::too_many_arguments)]
1158async fn create_annotation(
1159    entity_id: uuid::Uuid,
1160    annotation: &str,
1161    frame_position: Option<&[TyF64; 2]>,
1162    frame_plane_id: uuid::Uuid,
1163    leader_scale: Option<&TyF64>,
1164    font_size: Option<&TyF64>,
1165    exec_state: &mut ExecState,
1166    args: &Args,
1167    annotations: &mut Vec<GdtAnnotation>,
1168) -> Result<(), KclError> {
1169    let meta = vec![Metadata::from(args.source_range)];
1170    let annotation_id = exec_state.next_uuid();
1171    let feature_control = AnnotationFeatureControl::builder()
1172        .entity_id(entity_id)
1173        .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1174        .leader_type(AnnotationLineEnd::Dot)
1175        .prefix(annotation.to_owned())
1176        .plane_id(frame_plane_id)
1177        .offset(if let Some(offset) = frame_position {
1178            KPoint2d {
1179                x: offset[0].to_mm(),
1180                y: offset[1].to_mm(),
1181            }
1182        } else {
1183            KPoint2d { x: 100.0, y: 100.0 }
1184        })
1185        .precision(0)
1186        .font_scale(gdt_font_scale(font_size, args)?)
1187        .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1188        .leader_scale(leader_scale.map(|n| n.n as f32).unwrap_or(1.0))
1189        .build();
1190    let options = AnnotationOptions::builder().feature_control(feature_control).build();
1191    exec_state
1192        .batch_modeling_cmd(
1193            ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1194            ModelingCmd::from(
1195                mcmd::NewAnnotation::builder()
1196                    .options(options)
1197                    .clobber(false)
1198                    .annotation_type(AnnotationType::T3D)
1199                    .build(),
1200            ),
1201        )
1202        .await?;
1203    add_gdt_annotation_artifact(exec_state, args, annotation_id);
1204    annotations.push(GdtAnnotation {
1205        id: annotation_id,
1206        meta,
1207    });
1208    Ok(())
1209}
1210
1211fn gdt_control_frame(symbol: MbdSymbol, tolerance: f64, datums: &[char]) -> AnnotationMbdControlFrame {
1212    match datums {
1213        [] => AnnotationMbdControlFrame::builder()
1214            .symbol(symbol)
1215            .tolerance(tolerance)
1216            .build(),
1217        [primary] => AnnotationMbdControlFrame::builder()
1218            .symbol(symbol)
1219            .tolerance(tolerance)
1220            .primary_datum(*primary)
1221            .build(),
1222        [primary, secondary] => AnnotationMbdControlFrame::builder()
1223            .symbol(symbol)
1224            .tolerance(tolerance)
1225            .primary_datum(*primary)
1226            .secondary_datum(*secondary)
1227            .build(),
1228        [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1229            .symbol(symbol)
1230            .tolerance(tolerance)
1231            .primary_datum(*primary)
1232            .secondary_datum(*secondary)
1233            .tertiary_datum(*tertiary)
1234            .build(),
1235        _ => unreachable!("resolve_datums rejects more than three datums"),
1236    }
1237}
1238
1239fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1240    let datums = datums.unwrap_or_default();
1241    if datums.len() > 3 {
1242        return Err(KclError::new_semantic(KclErrorDetails::new(
1243            format!("{annotation_name} datums must include at most three names."),
1244            vec![args.source_range],
1245        )));
1246    }
1247
1248    let mut resolved = Vec::with_capacity(datums.len());
1249    for datum in &datums {
1250        let mut chars = datum.chars();
1251        let Some(name) = chars.next() else {
1252            return Err(KclError::new_semantic(KclErrorDetails::new(
1253                format!("{annotation_name} datum names must be a single character."),
1254                vec![args.source_range],
1255            )));
1256        };
1257        if chars.next().is_some() {
1258            return Err(KclError::new_semantic(KclErrorDetails::new(
1259                format!("{annotation_name} datum names must be a single character."),
1260                vec![args.source_range],
1261            )));
1262        }
1263        resolved.push(name);
1264    }
1265
1266    Ok(resolved)
1267}
1268
1269/// Get the XY plane by evaluating the `XY` expression so that it's the same as
1270/// if the user specified `XY`.
1271async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1272    let plane_ast = plane_ast("XY", args.source_range);
1273    let metadata = Metadata::from(args.source_range);
1274    let plane_value = args
1275        .ctx
1276        .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
1277        .await?;
1278    let plane_value = match plane_value.control {
1279        ControlFlowKind::Continue => plane_value.into_value(),
1280        ControlFlowKind::Exit => {
1281            let message = "Early return inside plane value is currently not supported".to_owned();
1282            debug_assert!(false, "{}", &message);
1283            return Err(KclError::new_internal(KclErrorDetails::new(
1284                message,
1285                vec![args.source_range],
1286            )));
1287        }
1288    };
1289    Ok(plane_value
1290        .as_plane()
1291        .ok_or_else(|| {
1292            KclError::new_internal(KclErrorDetails::new(
1293                "Expected XY plane to be defined".to_owned(),
1294                vec![args.source_range],
1295            ))
1296        })?
1297        .clone())
1298}
1299
1300/// An AST node for a plane with the given name.
1301fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1302    ast::Node::new(
1303        ast::Expr::Name(Box::new(ast::Node::new(
1304            ast::Name {
1305                name: ast::Identifier::new(plane_name),
1306                path: Vec::new(),
1307                // TODO: We may want to set this to true once we implement it to
1308                // prevent it breaking if users redefine the identifier.
1309                abs_path: false,
1310                digest: None,
1311            },
1312            range.start(),
1313            range.end(),
1314            range.module_id(),
1315        ))),
1316        range.start(),
1317        range.end(),
1318        range.module_id(),
1319    )
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324    use super::*;
1325    use crate::ExecutorContext;
1326    use crate::execution::Artifact;
1327    use crate::execution::ExecutorSettings;
1328    use crate::execution::MockConfig;
1329
1330    #[test]
1331    fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1332        let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1333        assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1334
1335        let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1336        assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1337
1338        let inch_in_mm = 25.4;
1339        let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1340        assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1341    }
1342
1343    const GDT_DATUM_KCL: &str = r#"
1344blockProfile = sketch(on = XY) {
1345  edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
1346  edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
1347  edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
1348  edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
1349  coincident([edge1.end, edge2.start])
1350  coincident([edge2.end, edge3.start])
1351  coincident([edge3.end, edge4.start])
1352  coincident([edge4.end, edge1.start])
1353  horizontal(edge1)
1354  vertical(edge2)
1355  horizontal(edge3)
1356  vertical(edge4)
1357}
1358
1359block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
1360
1361gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
1362"#;
1363
1364    async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
1365        let settings = ExecutorSettings {
1366            skip_artifact_graph,
1367            ..Default::default()
1368        };
1369        let ctx = ExecutorContext::new_mock(Some(settings)).await;
1370        let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
1371        let mock_config = MockConfig {
1372            use_prev_memory: false,
1373            ..Default::default()
1374        };
1375        let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
1376        ctx.close().await;
1377
1378        outcome
1379            .artifact_graph
1380            .values()
1381            .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
1382            .count()
1383    }
1384
1385    #[tokio::test(flavor = "multi_thread")]
1386    async fn gdt_annotations_follow_runtime_artifact_graph_setting() {
1387        assert_eq!(gdt_artifact_count(false).await, 1);
1388        assert_eq!(gdt_artifact_count(true).await, 0);
1389    }
1390}