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