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::AnnotationFeatureTag;
7use kittycad_modeling_cmds::shared::AnnotationLineEnd;
8use kittycad_modeling_cmds::shared::AnnotationMbdBasicDimension;
9use kittycad_modeling_cmds::shared::AnnotationMbdControlFrame;
10use kittycad_modeling_cmds::shared::AnnotationOptions;
11use kittycad_modeling_cmds::shared::AnnotationType;
12use kittycad_modeling_cmds::shared::MbdSymbol;
13use kittycad_modeling_cmds::shared::Point2d as KPoint2d;
14use kittycad_modeling_cmds::{self as kcmc};
15
16use crate::ExecState;
17use crate::KclError;
18use crate::errors::KclErrorDetails;
19use crate::exec::KclValue;
20use crate::execution::Artifact;
21use crate::execution::ArtifactId;
22use crate::execution::CodeRef;
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::TagIdentifier;
30use crate::execution::types::ArrayLen;
31use crate::execution::types::RuntimeType;
32use crate::parsing::ast::types as ast;
33use crate::parsing::ast::types::BoxNode;
34use crate::std::Args;
35use crate::std::args::FromKclValue;
36use crate::std::args::TyF64;
37use crate::std::edge;
38use crate::std::fillet::EdgeReference;
39use crate::std::sketch::ensure_sketch_plane_in_engine;
40use crate::unit_conversion::ToKcmc;
41
42const GDT_FONT_TEXTURE_POINT_SIZE: u32 = 36;
48const DEFAULT_GDT_FONT_SIZE_MM: f64 = 10.0;
49const DEFAULT_GDT_DOT_LEADER_SCALE: f64 = 1.0;
50const DEFAULT_GDT_DIMENSION_LEADER_SCALE: f64 = 1.0;
51const GDT_DOT_LEADER_REFERENCE_FONT_SIZE_MM: f64 = 100.0;
52const GDT_DOT_LEADER_REFERENCE_ENGINE_SCALE: f64 = 0.5;
53
54const GDT_FONT_SCALE_1_HEIGHT_MM: f64 = 8.0;
58
59fn gdt_font_scale(font_size: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
60 let requested_height_mm = font_size.map(TyF64::to_mm).unwrap_or(DEFAULT_GDT_FONT_SIZE_MM);
61 if requested_height_mm <= 0.0 {
62 return Err(KclError::new_semantic(KclErrorDetails::new(
63 "fontSize must be greater than 0.".to_owned(),
64 vec![args.source_range],
65 )));
66 }
67 Ok(gdt_font_scale_for_height_mm(requested_height_mm))
68}
69
70fn gdt_font_scale_for_height_mm(requested_height_mm: f64) -> f32 {
71 (requested_height_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32
72}
73
74fn gdt_user_leader_scale(leader_scale: Option<&TyF64>, default_scale: f64, args: &Args) -> Result<f32, KclError> {
75 let scale = leader_scale.map(|scale| scale.n).unwrap_or(default_scale);
76 if scale <= 0.0 {
77 return Err(KclError::new_semantic(KclErrorDetails::new(
78 "leaderScale must be greater than 0.".to_owned(),
79 vec![args.source_range],
80 )));
81 }
82 Ok(scale as f32)
83}
84
85fn gdt_dot_leader_scale(leader_scale: Option<&TyF64>, font_size: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
86 let user_scale = gdt_user_leader_scale(leader_scale, DEFAULT_GDT_DOT_LEADER_SCALE, args)?;
87 Ok(user_scale * gdt_dot_leader_normal_size() / gdt_font_scale(font_size, args)?)
90}
91
92fn gdt_dot_leader_normal_size() -> f32 {
93 gdt_font_scale_for_height_mm(GDT_DOT_LEADER_REFERENCE_FONT_SIZE_MM) * GDT_DOT_LEADER_REFERENCE_ENGINE_SCALE as f32
94}
95
96fn gdt_dimension_leader_scale(leader_scale: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
97 gdt_user_leader_scale(leader_scale, DEFAULT_GDT_DIMENSION_LEADER_SCALE, args)
98}
99
100#[derive(Debug, Clone)]
101enum DistanceEntity {
102 Face(Box<Face>),
103 TaggedFace(Box<TagIdentifier>),
104 Edge(EdgeReference),
105 Specifier(kcmc::shared::EdgeSpecifier),
106}
107
108#[derive(Debug, Clone)]
109enum GdtEdgeReference {
110 Entity(EdgeReference),
111 Specifier(kcmc::shared::EdgeSpecifier),
112}
113
114#[derive(Debug, Clone)]
115struct DistanceEndpoint {
116 entity_id: Option<uuid::Uuid>,
117 edge_reference: Option<kcmc::shared::EdgeSpecifier>,
118 entity_pos: KPoint2d<f64>,
119}
120
121#[derive(Debug, Clone, Copy)]
122enum GdtFeatureControlKind {
123 Flatness,
124 Straightness,
125 Circularity,
126 Cylindricity,
127 Concentricity,
128 Symmetry,
129 Runout,
130 ProfileLine,
131 ProfileSurface,
132 Position,
133 Angularity,
134 Perpendicularity,
135 Parallelism,
136}
137
138struct GdtFeatureControlParams {
139 faces: Vec<TagIdentifier>,
140 edges: Vec<GdtEdgeReference>,
141 datums: Option<Vec<String>>,
142 tolerance: TyF64,
143 precision: Option<TyF64>,
144 frame_position: Option<[TyF64; 2]>,
145 frame_plane: Option<Plane>,
146 leader_scale: Option<TyF64>,
147 font_size: Option<TyF64>,
148}
149
150struct GdtProfileCommonParams {
151 datums: Option<Vec<String>>,
152 tolerance: TyF64,
153 precision: Option<TyF64>,
154 frame_position: Option<[TyF64; 2]>,
155 frame_plane: Option<Plane>,
156 leader_scale: Option<TyF64>,
157 font_size: Option<TyF64>,
158}
159
160impl GdtFeatureControlKind {
161 fn label(self) -> &'static str {
162 match self {
163 Self::Flatness => "Flatness",
164 Self::Straightness => "Straightness",
165 Self::Circularity => "Circularity",
166 Self::Cylindricity => "Cylindricity",
167 Self::Concentricity => "Concentricity",
168 Self::Symmetry => "Symmetry",
169 Self::Runout => "Runout",
170 Self::ProfileLine => "Profile line",
171 Self::ProfileSurface => "Profile surface",
172 Self::Position => "Position",
173 Self::Angularity => "Angularity",
174 Self::Perpendicularity => "Perpendicularity",
175 Self::Parallelism => "Parallelism",
176 }
177 }
178
179 fn symbol(self) -> MbdSymbol {
180 match self {
181 Self::Flatness => MbdSymbol::Flatness,
182 Self::Straightness => MbdSymbol::Straightness,
183 Self::Circularity => MbdSymbol::Roundness,
184 Self::Cylindricity => MbdSymbol::Cylindricity,
185 Self::Concentricity => MbdSymbol::Concentricity,
186 Self::Symmetry => MbdSymbol::Symmetry,
187 Self::Runout => MbdSymbol::Runout,
188 Self::ProfileLine => MbdSymbol::ProfileOfLine,
189 Self::ProfileSurface => MbdSymbol::SurfaceProfile,
190 Self::Position => MbdSymbol::Position,
191 Self::Angularity => MbdSymbol::Angularity,
192 Self::Perpendicularity => MbdSymbol::Perpendicularity,
193 Self::Parallelism => MbdSymbol::Parallelism,
194 }
195 }
196
197 fn diameter_symbol(self) -> Option<MbdSymbol> {
198 match self {
199 Self::Concentricity => Some(MbdSymbol::Diameter),
200 _ => None,
201 }
202 }
203
204 fn requires_datums(self) -> bool {
205 matches!(self, Self::Concentricity | Self::Symmetry | Self::Runout)
206 }
207}
208
209fn add_gdt_annotation_artifact(exec_state: &mut ExecState, args: &Args, annotation_id: uuid::Uuid) {
210 exec_state.add_artifact(Artifact::GdtAnnotation(GdtAnnotationArtifact {
211 id: ArtifactId::new(annotation_id),
212 code_ref: CodeRef::placeholder(args.source_range),
213 }));
214}
215
216impl DistanceEntity {
217 async fn to_endpoint(&self, exec_state: &mut ExecState, args: &Args) -> Result<DistanceEndpoint, KclError> {
218 match self {
219 DistanceEntity::Face(face) => Ok(DistanceEndpoint {
220 entity_id: Some(face.id),
221 edge_reference: None,
222 entity_pos: KPoint2d { x: 0.5, y: 0.5 },
223 }),
224 DistanceEntity::TaggedFace(face) => Ok(DistanceEndpoint {
225 entity_id: Some(args.get_adjacent_face_to_tag(exec_state, face, false).await?),
226 edge_reference: None,
227 entity_pos: KPoint2d { x: 0.5, y: 0.5 },
228 }),
229 DistanceEntity::Edge(edge) => Ok(DistanceEndpoint {
230 entity_id: Some(edge.get_engine_id(exec_state, args)?),
231 edge_reference: None,
232 entity_pos: KPoint2d { x: 0.5, y: 0.0 },
233 }),
234 DistanceEntity::Specifier(edge_reference) => Ok(DistanceEndpoint {
235 entity_id: None,
236 edge_reference: Some(edge_reference.clone()),
237 entity_pos: KPoint2d { x: 0.5, y: 0.0 },
238 }),
239 }
240 }
241}
242
243impl<'a> FromKclValue<'a> for DistanceEntity {
244 fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
245 match arg {
246 KclValue::Face { value } => Some(Self::Face(value.to_owned())),
247 KclValue::Uuid { value, .. } => Some(Self::Edge(EdgeReference::Uuid(*value))),
248 KclValue::TagIdentifier(value) => Some(Self::TaggedFace(value.to_owned())),
249 _ => None,
250 }
251 }
252}
253
254async fn parse_distance_entity_arg(
255 arg_name: &str,
256 exec_state: &mut ExecState,
257 args: &Args,
258) -> Result<Option<DistanceEntity>, KclError> {
259 let Some(value): Option<KclValue> = args.get_kw_arg_opt(arg_name, &RuntimeType::any(), exec_state)? else {
260 return Ok(None);
261 };
262
263 if edge::is_edge_specifier_object(&value) {
264 let unresolved = edge::parse_edge_specifier_value(&value, args)?;
265 let edge_reference = edge::resolve_edge_specifier_with_face_tags(&unresolved, None, exec_state, args).await?;
266 return Ok(Some(DistanceEntity::Specifier(edge_reference)));
267 }
268
269 DistanceEntity::from_kcl_val(&value)
270 .map(Some)
271 .ok_or_else(|| {
272 KclError::new_type(KclErrorDetails::new(
273 format!(
274 "`{arg_name}` must be a face, tagged face, tagged edge, edge UUID, or edge specifier object (e.g. {{ sideFaces = [...], endFaces = [...], index = 0 }})"
275 ),
276 vec![args.source_range],
277 ))
278 })
279}
280
281async fn parse_gdt_edges_arg(exec_state: &mut ExecState, args: &Args) -> Result<Vec<GdtEdgeReference>, KclError> {
282 let Some(edges): Option<Vec<KclValue>> = args.get_kw_arg_opt("edges", &RuntimeType::any_array(), exec_state)?
285 else {
286 return Ok(Vec::new());
287 };
288
289 if edges.is_empty() {
290 return Err(KclError::new_semantic(KclErrorDetails::new(
291 "`edges` must contain at least one edge.".to_owned(),
292 vec![args.source_range],
293 )));
294 }
295
296 let mut parsed_edges = Vec::with_capacity(edges.len());
297 for edge_value in &edges {
298 if edge::is_edge_specifier_object(edge_value) {
299 let unresolved = edge::parse_edge_specifier_value(edge_value, args)?;
300 let edge_reference =
301 edge::resolve_edge_specifier_with_face_tags(&unresolved, None, exec_state, args).await?;
302 parsed_edges.push(GdtEdgeReference::Specifier(edge_reference));
303 } else if let Some(edge) = EdgeReference::from_kcl_val(edge_value) {
304 parsed_edges.push(GdtEdgeReference::Entity(edge));
305 } else {
306 return Err(KclError::new_type(KclErrorDetails::new(
307 "edges must contain tagged edges, edge UUIDs, or edge specifier objects (e.g. { sideFaces = [...], endFaces = [...], index = 0 })".to_owned(),
308 vec![args.source_range],
309 )));
310 }
311 }
312 Ok(parsed_edges)
313}
314
315pub async fn datum(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
316 let face: TagIdentifier = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
317 let name: String = args.get_kw_arg("name", &RuntimeType::string(), exec_state)?;
318 let frame_position: Option<[TyF64; 2]> =
319 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
320 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
321 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
322 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
323
324 let annotation = inner_datum(
325 face,
326 name,
327 frame_position,
328 frame_plane,
329 leader_scale,
330 font_size,
331 exec_state,
332 &args,
333 )
334 .await?;
335 Ok(KclValue::GdtAnnotation {
336 value: Box::new(annotation),
337 })
338}
339
340#[allow(clippy::too_many_arguments)]
341async fn inner_datum(
342 face: TagIdentifier,
343 name: String,
344 frame_position: Option<[TyF64; 2]>,
345 frame_plane: Option<Plane>,
346 leader_scale: Option<TyF64>,
347 font_size: Option<TyF64>,
348 exec_state: &mut ExecState,
349 args: &Args,
350) -> Result<GdtAnnotation, KclError> {
351 const DATUM_LENGTH_ERROR: &str = "Datum name must be a single character.";
352 if name.len() > 1 {
353 return Err(KclError::new_semantic(KclErrorDetails::new(
354 DATUM_LENGTH_ERROR.to_owned(),
355 vec![args.source_range],
356 )));
357 }
358 let name_char = name.chars().next().ok_or_else(|| {
359 KclError::new_semantic(KclErrorDetails::new(
360 DATUM_LENGTH_ERROR.to_owned(),
361 vec![args.source_range],
362 ))
363 })?;
364 let mut frame_plane = if let Some(plane) = frame_plane {
365 plane
366 } else {
367 xy_plane(exec_state, args).await?
369 };
370 ensure_sketch_plane_in_engine(
371 &mut frame_plane,
372 exec_state,
373 &args.ctx,
374 args.source_range,
375 args.node_path.clone(),
376 )
377 .await?;
378 let face_id = args.get_adjacent_face_to_tag(exec_state, &face, false).await?;
379 let meta = vec![Metadata::from(args.source_range)];
380 let annotation_id = exec_state.next_uuid();
381 let feature_control = AnnotationFeatureControl::builder()
382 .maybe_entity_id(Some(face_id))
383 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
385 .leader_type(AnnotationLineEnd::Dot)
386 .defined_datum(name_char)
387 .plane_id(frame_plane.id)
388 .offset(if let Some(offset) = &frame_position {
389 KPoint2d {
390 x: offset[0].to_mm(),
391 y: offset[1].to_mm(),
392 }
393 } else {
394 KPoint2d { x: 100.0, y: 100.0 }
395 })
396 .precision(0)
397 .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
398 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
399 .leader_scale(gdt_dot_leader_scale(leader_scale.as_ref(), font_size.as_ref(), args)?)
400 .build();
401 exec_state
402 .batch_modeling_cmd(
403 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
404 ModelingCmd::from(
405 mcmd::NewAnnotation::builder()
406 .options(AnnotationOptions::builder().feature_control(feature_control).build())
407 .clobber(false)
408 .annotation_type(AnnotationType::T3D)
409 .build(),
410 ),
411 )
412 .await?;
413 add_gdt_annotation_artifact(exec_state, args, annotation_id);
414 Ok(GdtAnnotation {
415 id: annotation_id,
416 meta,
417 })
418}
419
420pub async fn note(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
421 let note: String = args.get_kw_arg("note", &RuntimeType::string(), exec_state)?;
422 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
423 let frame_position: Option<[TyF64; 2]> =
424 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
425 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
426
427 let annotation = inner_note(note, frame_plane, frame_position, font_size, exec_state, &args).await?;
428 Ok(KclValue::GdtAnnotation {
429 value: Box::new(annotation),
430 })
431}
432
433async fn inner_note(
434 note: String,
435 frame_plane: Option<Plane>,
436 frame_position: Option<[TyF64; 2]>,
437 font_size: Option<TyF64>,
438 exec_state: &mut ExecState,
439 args: &Args,
440) -> Result<GdtAnnotation, KclError> {
441 let mut frame_plane = if let Some(plane) = frame_plane {
442 plane
443 } else {
444 xy_plane(exec_state, args).await?
446 };
447 ensure_sketch_plane_in_engine(
448 &mut frame_plane,
449 exec_state,
450 &args.ctx,
451 args.source_range,
452 args.node_path.clone(),
453 )
454 .await?;
455 let meta = vec![Metadata::from(args.source_range)];
456 let annotation_id = exec_state.next_uuid();
457 let feature_tag = AnnotationFeatureTag::builder()
460 .maybe_entity_id(Some(frame_plane.id))
461 .entity_pos(KPoint2d { x: 0.0, y: 0.0 })
462 .leader_type(AnnotationLineEnd::None)
463 .key(String::new())
464 .value(note)
465 .show_key(false)
466 .plane_id(frame_plane.id)
467 .offset(if let Some(offset) = &frame_position {
468 KPoint2d {
469 x: offset[0].to_mm(),
470 y: offset[1].to_mm(),
471 }
472 } else {
473 KPoint2d { x: 100.0, y: 100.0 }
474 })
475 .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
476 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
477 .leader_scale(1.0)
478 .build();
479 exec_state
480 .batch_modeling_cmd(
481 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
482 ModelingCmd::from(
483 mcmd::NewAnnotation::builder()
484 .options(AnnotationOptions::builder().feature_tag(feature_tag).build())
485 .clobber(false)
486 .annotation_type(AnnotationType::T3D)
487 .build(),
488 ),
489 )
490 .await?;
491 add_gdt_annotation_artifact(exec_state, args, annotation_id);
492 Ok(GdtAnnotation {
493 id: annotation_id,
494 meta,
495 })
496}
497
498pub async fn flatness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
499 let faces: Vec<TagIdentifier> = args.get_kw_arg(
500 "faces",
501 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
502 exec_state,
503 )?;
504 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
505 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
506 let frame_position: Option<[TyF64; 2]> =
507 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
508 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
509 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
510 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
511
512 let annotations = create_feature_control_annotations(
513 GdtFeatureControlKind::Flatness,
514 GdtFeatureControlParams {
515 faces,
516 edges: Vec::new(),
517 datums: None,
518 tolerance,
519 precision,
520 frame_position,
521 frame_plane,
522 leader_scale,
523 font_size,
524 },
525 exec_state,
526 &args,
527 )
528 .await?;
529 Ok(annotations.into())
530}
531
532pub async fn straightness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
533 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
534 "faces",
535 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
536 exec_state,
537 )?;
538 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
539 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
540 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
541 let frame_position: Option<[TyF64; 2]> =
542 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
543 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
544 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
545 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
546
547 let annotations = create_feature_control_annotations(
548 GdtFeatureControlKind::Straightness,
549 GdtFeatureControlParams {
550 faces: faces.unwrap_or_default(),
551 edges,
552 datums: None,
553 tolerance,
554 precision,
555 frame_position,
556 frame_plane,
557 leader_scale,
558 font_size,
559 },
560 exec_state,
561 &args,
562 )
563 .await?;
564 Ok(annotations.into())
565}
566
567pub async fn circularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
568 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
569 "faces",
570 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
571 exec_state,
572 )?;
573 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
574 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
575 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
576 let frame_position: Option<[TyF64; 2]> =
577 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
578 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
579 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
580 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
581
582 let annotations = create_feature_control_annotations(
583 GdtFeatureControlKind::Circularity,
584 GdtFeatureControlParams {
585 faces: faces.unwrap_or_default(),
586 edges,
587 datums: None,
588 tolerance,
589 precision,
590 frame_position,
591 frame_plane,
592 leader_scale,
593 font_size,
594 },
595 exec_state,
596 &args,
597 )
598 .await?;
599 Ok(annotations.into())
600}
601
602pub async fn cylindricity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
603 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
604 "faces",
605 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
606 exec_state,
607 )?;
608 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
609 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
610 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
611 let frame_position: Option<[TyF64; 2]> =
612 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
613 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
614 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
615 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
616
617 let annotations = create_feature_control_annotations(
618 GdtFeatureControlKind::Cylindricity,
619 GdtFeatureControlParams {
620 faces: faces.unwrap_or_default(),
621 edges,
622 datums: None,
623 tolerance,
624 precision,
625 frame_position,
626 frame_plane,
627 leader_scale,
628 font_size,
629 },
630 exec_state,
631 &args,
632 )
633 .await?;
634 Ok(annotations.into())
635}
636
637pub async fn concentricity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
638 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
639 "faces",
640 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
641 exec_state,
642 )?;
643 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
644 let datums: Vec<String> = args.get_kw_arg(
645 "datums",
646 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
647 exec_state,
648 )?;
649 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
650 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
651 let frame_position: Option<[TyF64; 2]> =
652 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
653 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
654 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
655 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
656
657 let annotations = create_feature_control_annotations(
658 GdtFeatureControlKind::Concentricity,
659 GdtFeatureControlParams {
660 faces: faces.unwrap_or_default(),
661 edges,
662 datums: Some(datums),
663 tolerance,
664 precision,
665 frame_position,
666 frame_plane,
667 leader_scale,
668 font_size,
669 },
670 exec_state,
671 &args,
672 )
673 .await?;
674 Ok(annotations.into())
675}
676
677pub async fn symmetry(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
678 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
679 "faces",
680 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
681 exec_state,
682 )?;
683 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
684 let datums: Vec<String> = args.get_kw_arg(
685 "datums",
686 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
687 exec_state,
688 )?;
689 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
690 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
691 let frame_position: Option<[TyF64; 2]> =
692 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
693 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
694 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
695 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
696
697 let annotations = create_feature_control_annotations(
698 GdtFeatureControlKind::Symmetry,
699 GdtFeatureControlParams {
700 faces: faces.unwrap_or_default(),
701 edges,
702 datums: Some(datums),
703 tolerance,
704 precision,
705 frame_position,
706 frame_plane,
707 leader_scale,
708 font_size,
709 },
710 exec_state,
711 &args,
712 )
713 .await?;
714 Ok(annotations.into())
715}
716
717pub async fn runout(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
718 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
719 "faces",
720 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
721 exec_state,
722 )?;
723 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
724 let datums: Vec<String> = args.get_kw_arg(
725 "datums",
726 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
727 exec_state,
728 )?;
729 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
730 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
731 let frame_position: Option<[TyF64; 2]> =
732 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
733 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
734 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
735 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
736
737 let annotations = create_feature_control_annotations(
738 GdtFeatureControlKind::Runout,
739 GdtFeatureControlParams {
740 faces: faces.unwrap_or_default(),
741 edges,
742 datums: Some(datums),
743 tolerance,
744 precision,
745 frame_position,
746 frame_plane,
747 leader_scale,
748 font_size,
749 },
750 exec_state,
751 &args,
752 )
753 .await?;
754 Ok(annotations.into())
755}
756
757pub async fn profile_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
758 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
759 let params = profile_common_params(&args, exec_state)?;
760
761 let annotations = inner_profile_line(edges, params, exec_state, &args).await?;
762 Ok(annotations.into())
763}
764
765pub async fn profile_surface(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
766 let faces: Vec<TagIdentifier> = args.get_kw_arg(
767 "faces",
768 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
769 exec_state,
770 )?;
771 let params = profile_common_params(&args, exec_state)?;
772
773 let annotations = inner_profile_surface(faces, params, exec_state, &args).await?;
774 Ok(annotations.into())
775}
776
777pub async fn profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
783 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
784 "faces",
785 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
786 exec_state,
787 )?;
788 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
789
790 let annotations = match (!edges.is_empty(), faces) {
791 (true, None) => {
792 let params = profile_common_params(&args, exec_state)?;
793 inner_profile_line(edges, params, exec_state, &args).await?
794 }
795 (false, Some(faces)) => {
796 let params = profile_common_params(&args, exec_state)?;
797 inner_profile_surface(faces, params, exec_state, &args).await?
798 }
799 (true, Some(_)) => {
800 return Err(KclError::new_semantic(KclErrorDetails::new(
801 "Profile cannot combine `edges` and `faces`. Use `profileLine` for edges or `profileSurface` for faces."
802 .to_owned(),
803 vec![args.source_range],
804 )));
805 }
806 (false, None) => {
807 return Err(KclError::new_semantic(KclErrorDetails::new(
808 "Profile requires either `edges` for `profileLine` or `faces` for `profileSurface`.".to_owned(),
809 vec![args.source_range],
810 )));
811 }
812 };
813
814 Ok(annotations.into())
815}
816
817fn profile_common_params(args: &Args, exec_state: &mut ExecState) -> Result<GdtProfileCommonParams, KclError> {
818 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
819 "datums",
820 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
821 exec_state,
822 )?;
823 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
824 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
825 let frame_position: Option<[TyF64; 2]> =
826 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
827 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
828 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
829 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
830
831 Ok(GdtProfileCommonParams {
832 datums,
833 tolerance,
834 precision,
835 frame_position,
836 frame_plane,
837 leader_scale,
838 font_size,
839 })
840}
841
842async fn inner_profile_line(
843 edges: Vec<GdtEdgeReference>,
844 params: GdtProfileCommonParams,
845 exec_state: &mut ExecState,
846 args: &Args,
847) -> Result<Vec<GdtAnnotation>, KclError> {
848 create_feature_control_annotations(
849 GdtFeatureControlKind::ProfileLine,
850 GdtFeatureControlParams {
851 faces: Vec::new(),
852 edges,
853 datums: params.datums,
854 tolerance: params.tolerance,
855 precision: params.precision,
856 frame_position: params.frame_position,
857 frame_plane: params.frame_plane,
858 leader_scale: params.leader_scale,
859 font_size: params.font_size,
860 },
861 exec_state,
862 args,
863 )
864 .await
865}
866
867async fn inner_profile_surface(
868 faces: Vec<TagIdentifier>,
869 params: GdtProfileCommonParams,
870 exec_state: &mut ExecState,
871 args: &Args,
872) -> Result<Vec<GdtAnnotation>, KclError> {
873 create_feature_control_annotations(
874 GdtFeatureControlKind::ProfileSurface,
875 GdtFeatureControlParams {
876 faces,
877 edges: Vec::new(),
878 datums: params.datums,
879 tolerance: params.tolerance,
880 precision: params.precision,
881 frame_position: params.frame_position,
882 frame_plane: params.frame_plane,
883 leader_scale: params.leader_scale,
884 font_size: params.font_size,
885 },
886 exec_state,
887 args,
888 )
889 .await
890}
891
892pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
893 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
894 "faces",
895 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
896 exec_state,
897 )?;
898 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
899 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
900 "datums",
901 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
902 exec_state,
903 )?;
904 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
905 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
906 let frame_position: Option<[TyF64; 2]> =
907 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
908 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
909 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
910 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
911
912 let annotations = create_feature_control_annotations(
913 GdtFeatureControlKind::Position,
914 GdtFeatureControlParams {
915 faces: faces.unwrap_or_default(),
916 edges,
917 datums,
918 tolerance,
919 precision,
920 frame_position,
921 frame_plane,
922 leader_scale,
923 font_size,
924 },
925 exec_state,
926 &args,
927 )
928 .await?;
929 Ok(annotations.into())
930}
931
932pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
933 let from = parse_distance_entity_arg("from", exec_state, &args).await?;
934 let to = parse_distance_entity_arg("to", exec_state, &args).await?;
935 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
936 let tolerance = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
937 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
938 let frame_position: Option<[TyF64; 2]> =
939 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
940 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
941 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
942 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
943
944 let annotations = inner_distance(
945 from,
946 to,
947 edges,
948 tolerance,
949 precision,
950 frame_position,
951 frame_plane,
952 leader_scale,
953 font_size,
954 exec_state,
955 &args,
956 )
957 .await?;
958 Ok(annotations.into())
959}
960
961#[allow(clippy::too_many_arguments)]
962async fn inner_distance(
963 from: Option<DistanceEntity>,
964 to: Option<DistanceEntity>,
965 edges: Vec<GdtEdgeReference>,
966 tolerance: Option<TyF64>,
967 precision: Option<TyF64>,
968 frame_position: Option<[TyF64; 2]>,
969 frame_plane: Option<Plane>,
970 leader_scale: Option<TyF64>,
971 font_size: Option<TyF64>,
972 exec_state: &mut ExecState,
973 args: &Args,
974) -> Result<Vec<GdtAnnotation>, KclError> {
975 let precision = resolve_precision(precision, args)?;
976 let mut frame_plane = if let Some(plane) = frame_plane {
977 plane
978 } else {
979 xy_plane(exec_state, args).await?
980 };
981 ensure_sketch_plane_in_engine(
982 &mut frame_plane,
983 exec_state,
984 &args.ctx,
985 args.source_range,
986 args.node_path.clone(),
987 )
988 .await?;
989
990 if from.is_some() || to.is_some() {
991 if !edges.is_empty() {
992 return Err(KclError::new_semantic(KclErrorDetails::new(
993 "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
994 vec![args.source_range],
995 )));
996 }
997
998 let (Some(from), Some(to)) = (from, to) else {
999 return Err(KclError::new_semantic(KclErrorDetails::new(
1000 "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
1001 vec![args.source_range],
1002 )));
1003 };
1004
1005 let from = from.to_endpoint(exec_state, args).await?;
1006 let to = to.to_endpoint(exec_state, args).await?;
1007 let mut annotations = Vec::with_capacity(1);
1008 create_basic_distance_annotation(
1009 from,
1010 to,
1011 &tolerance,
1012 precision,
1013 frame_position.as_ref(),
1014 frame_plane.id,
1015 leader_scale.as_ref(),
1016 font_size.as_ref(),
1017 exec_state,
1018 args,
1019 &mut annotations,
1020 )
1021 .await?;
1022 return Ok(annotations);
1023 }
1024
1025 if edges.is_empty() {
1026 return Err(KclError::new_semantic(KclErrorDetails::new(
1027 "Distance requires either `edges` or both `from` and `to`.".to_owned(),
1028 vec![args.source_range],
1029 )));
1030 }
1031
1032 let mut annotations = Vec::with_capacity(edges.len());
1033 for edge in &edges {
1034 let (entity_id, edge_reference) = match edge {
1035 GdtEdgeReference::Entity(edge) => (Some(edge.get_engine_id(exec_state, args)?), None),
1036 GdtEdgeReference::Specifier(edge_reference) => (None, Some(edge_reference.clone())),
1037 };
1038 create_basic_distance_annotation(
1039 DistanceEndpoint {
1040 entity_id,
1041 edge_reference: edge_reference.clone(),
1042 entity_pos: KPoint2d { x: 0.0, y: 0.0 },
1043 },
1044 DistanceEndpoint {
1045 entity_id,
1046 edge_reference,
1047 entity_pos: KPoint2d { x: 1.0, y: 0.0 },
1048 },
1049 &tolerance,
1050 precision,
1051 frame_position.as_ref(),
1052 frame_plane.id,
1053 leader_scale.as_ref(),
1054 font_size.as_ref(),
1055 exec_state,
1056 args,
1057 &mut annotations,
1058 )
1059 .await?;
1060 }
1061 Ok(annotations)
1062}
1063
1064#[allow(clippy::too_many_arguments)]
1065async fn create_basic_distance_annotation(
1066 from: DistanceEndpoint,
1067 to: DistanceEndpoint,
1068 tolerance: &Option<TyF64>,
1069 precision: u32,
1070 frame_position: Option<&[TyF64; 2]>,
1071 frame_plane_id: uuid::Uuid,
1072 leader_scale: Option<&TyF64>,
1073 font_size: Option<&TyF64>,
1074 exec_state: &mut ExecState,
1075 args: &Args,
1076 annotations: &mut Vec<GdtAnnotation>,
1077) -> Result<(), KclError> {
1078 let meta = vec![Metadata::from(args.source_range)];
1079 let annotation_id = exec_state.next_uuid();
1080 let display_units = exec_state.length_unit();
1081 let dimension = AnnotationBasicDimension::builder()
1082 .maybe_from_entity_id(from.entity_id)
1083 .maybe_from_edge_reference(from.edge_reference)
1084 .from_entity_pos(from.entity_pos)
1085 .maybe_to_entity_id(to.entity_id)
1086 .maybe_to_edge_reference(to.edge_reference)
1087 .to_entity_pos(to.entity_pos)
1088 .dimension(
1089 AnnotationMbdBasicDimension::builder()
1090 .tolerance(
1091 tolerance
1092 .as_ref()
1093 .map(|tol| tol.to_length_units(display_units))
1094 .unwrap_or_default(),
1095 )
1096 .build(),
1097 )
1098 .plane_id(frame_plane_id)
1099 .offset(if let Some(offset) = frame_position {
1100 KPoint2d {
1101 x: offset[0].to_mm(),
1102 y: offset[1].to_mm(),
1103 }
1104 } else {
1105 KPoint2d { x: 100.0, y: 100.0 }
1106 })
1107 .precision(precision)
1108 .font_scale(gdt_font_scale(font_size, args)?)
1109 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1110 .arrow_scale(gdt_dimension_leader_scale(leader_scale, args)?)
1111 .build();
1112 let options = AnnotationOptions::builder()
1113 .dimension(dimension)
1114 .units(display_units.to_kcmc())
1115 .build();
1116 let annotation_cmd = ModelingCmd::from(
1117 mcmd::NewAnnotation::builder()
1118 .options(options)
1119 .clobber(false)
1120 .annotation_type(AnnotationType::T3D)
1121 .build(),
1122 );
1123 let cmd_meta = ModelingCmdMeta::from_args_id(exec_state, args, annotation_id);
1124 exec_state.batch_modeling_cmd(cmd_meta, annotation_cmd).await?;
1125 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1126 annotations.push(GdtAnnotation {
1127 id: annotation_id,
1128 meta,
1129 });
1130 Ok(())
1131}
1132
1133pub async fn angularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1134 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1135 "faces",
1136 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1137 exec_state,
1138 )?;
1139 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1140 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1141 "datums",
1142 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1143 exec_state,
1144 )?;
1145 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1146 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1147 let frame_position: Option<[TyF64; 2]> =
1148 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1149 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1150 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1151 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1152
1153 let annotations = create_feature_control_annotations(
1154 GdtFeatureControlKind::Angularity,
1155 GdtFeatureControlParams {
1156 faces: faces.unwrap_or_default(),
1157 edges,
1158 datums,
1159 tolerance,
1160 precision,
1161 frame_position,
1162 frame_plane,
1163 leader_scale,
1164 font_size,
1165 },
1166 exec_state,
1167 &args,
1168 )
1169 .await?;
1170 Ok(annotations.into())
1171}
1172
1173pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1174 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1175 "faces",
1176 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1177 exec_state,
1178 )?;
1179 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1180 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1181 "datums",
1182 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1183 exec_state,
1184 )?;
1185 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1186 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1187 let frame_position: Option<[TyF64; 2]> =
1188 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1189 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1190 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1191 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1192
1193 let annotations = create_feature_control_annotations(
1194 GdtFeatureControlKind::Perpendicularity,
1195 GdtFeatureControlParams {
1196 faces: faces.unwrap_or_default(),
1197 edges,
1198 datums,
1199 tolerance,
1200 precision,
1201 frame_position,
1202 frame_plane,
1203 leader_scale,
1204 font_size,
1205 },
1206 exec_state,
1207 &args,
1208 )
1209 .await?;
1210 Ok(annotations.into())
1211}
1212
1213pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1214 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1215 "faces",
1216 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1217 exec_state,
1218 )?;
1219 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1220 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1221 "datums",
1222 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1223 exec_state,
1224 )?;
1225 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1226 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1227 let frame_position: Option<[TyF64; 2]> =
1228 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1229 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1230 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1231 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1232
1233 let annotations = create_feature_control_annotations(
1234 GdtFeatureControlKind::Parallelism,
1235 GdtFeatureControlParams {
1236 faces: faces.unwrap_or_default(),
1237 edges,
1238 datums,
1239 tolerance,
1240 precision,
1241 frame_position,
1242 frame_plane,
1243 leader_scale,
1244 font_size,
1245 },
1246 exec_state,
1247 &args,
1248 )
1249 .await?;
1250 Ok(annotations.into())
1251}
1252
1253pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1254 let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
1255 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1256 "faces",
1257 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1258 exec_state,
1259 )?;
1260 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1261 let frame_position: Option<[TyF64; 2]> =
1262 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1263 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1264 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1265 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1266
1267 let annotations = inner_annotation(
1268 annotation,
1269 faces.unwrap_or_default(),
1270 edges,
1271 frame_position,
1272 frame_plane,
1273 leader_scale,
1274 font_size,
1275 exec_state,
1276 &args,
1277 )
1278 .await?;
1279 Ok(annotations.into())
1280}
1281
1282#[allow(clippy::too_many_arguments)]
1283async fn inner_annotation(
1284 annotation: String,
1285 faces: Vec<TagIdentifier>,
1286 edges: Vec<GdtEdgeReference>,
1287 frame_position: Option<[TyF64; 2]>,
1288 frame_plane: Option<Plane>,
1289 leader_scale: Option<TyF64>,
1290 font_size: Option<TyF64>,
1291 exec_state: &mut ExecState,
1292 args: &Args,
1293) -> Result<Vec<GdtAnnotation>, KclError> {
1294 if annotation.is_empty() {
1295 return Err(KclError::new_semantic(KclErrorDetails::new(
1296 "Annotation text must not be empty.".to_owned(),
1297 vec![args.source_range],
1298 )));
1299 }
1300 if faces.is_empty() && edges.is_empty() {
1301 return Err(KclError::new_semantic(KclErrorDetails::new(
1302 "Annotation requires at least one face or edge.".to_owned(),
1303 vec![args.source_range],
1304 )));
1305 }
1306
1307 let mut frame_plane = if let Some(plane) = frame_plane {
1308 plane
1309 } else {
1310 xy_plane(exec_state, args).await?
1311 };
1312 ensure_sketch_plane_in_engine(
1313 &mut frame_plane,
1314 exec_state,
1315 &args.ctx,
1316 args.source_range,
1317 args.node_path.clone(),
1318 )
1319 .await?;
1320
1321 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1322 for face in &faces {
1323 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1324 create_annotation(
1325 Some(face_id),
1326 None,
1327 &annotation,
1328 frame_position.as_ref(),
1329 frame_plane.id,
1330 leader_scale.as_ref(),
1331 font_size.as_ref(),
1332 exec_state,
1333 args,
1334 &mut annotations,
1335 )
1336 .await?;
1337 }
1338 for edge in &edges {
1339 match edge {
1340 GdtEdgeReference::Entity(edge) => {
1341 create_annotation(
1342 Some(edge.get_engine_id(exec_state, args)?),
1343 None,
1344 &annotation,
1345 frame_position.as_ref(),
1346 frame_plane.id,
1347 leader_scale.as_ref(),
1348 font_size.as_ref(),
1349 exec_state,
1350 args,
1351 &mut annotations,
1352 )
1353 .await?;
1354 }
1355 GdtEdgeReference::Specifier(edge_reference) => {
1356 create_annotation(
1357 None,
1358 Some(edge_reference.clone()),
1359 &annotation,
1360 frame_position.as_ref(),
1361 frame_plane.id,
1362 leader_scale.as_ref(),
1363 font_size.as_ref(),
1364 exec_state,
1365 args,
1366 &mut annotations,
1367 )
1368 .await?;
1369 }
1370 }
1371 }
1372
1373 Ok(annotations)
1374}
1375
1376fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
1377 if let Some(precision) = precision {
1378 let rounded = precision.n.round();
1379 if !(0.0..=9.0).contains(&rounded) {
1380 return Err(KclError::new_semantic(KclErrorDetails::new(
1381 "Precision must be between 0 and 9".to_owned(),
1382 vec![args.source_range],
1383 )));
1384 }
1385 Ok(rounded as u32)
1386 } else {
1387 Ok(3)
1388 }
1389}
1390
1391async fn resolve_gdt_frame_plane(
1392 frame_plane: Option<Plane>,
1393 exec_state: &mut ExecState,
1394 args: &Args,
1395) -> Result<Plane, KclError> {
1396 let mut frame_plane = if let Some(plane) = frame_plane {
1397 plane
1398 } else {
1399 xy_plane(exec_state, args).await?
1401 };
1402 ensure_sketch_plane_in_engine(
1403 &mut frame_plane,
1404 exec_state,
1405 &args.ctx,
1406 args.source_range,
1407 args.node_path.clone(),
1408 )
1409 .await?;
1410 Ok(frame_plane)
1411}
1412
1413async fn create_feature_control_annotations(
1414 kind: GdtFeatureControlKind,
1415 params: GdtFeatureControlParams,
1416 exec_state: &mut ExecState,
1417 args: &Args,
1418) -> Result<Vec<GdtAnnotation>, KclError> {
1419 let GdtFeatureControlParams {
1420 faces,
1421 edges,
1422 datums,
1423 tolerance,
1424 precision,
1425 frame_position,
1426 frame_plane,
1427 leader_scale,
1428 font_size,
1429 } = params;
1430
1431 if faces.is_empty() && edges.is_empty() {
1432 return Err(KclError::new_semantic(KclErrorDetails::new(
1433 format!("{} requires at least one face or edge.", kind.label()),
1434 vec![args.source_range],
1435 )));
1436 }
1437
1438 let precision = resolve_precision(precision, args)?;
1439 let datums = resolve_datums(datums, args, kind.label())?;
1440 if kind.requires_datums() && datums.is_empty() {
1441 return Err(KclError::new_semantic(KclErrorDetails::new(
1442 format!("{} requires at least one datum.", kind.label()),
1443 vec![args.source_range],
1444 )));
1445 }
1446 let frame_plane = resolve_gdt_frame_plane(frame_plane, exec_state, args).await?;
1447 let symbol = kind.symbol();
1448 let diameter_symbol = kind.diameter_symbol();
1449
1450 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1451 for face in &faces {
1452 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1453 create_feature_control_annotation(
1454 Some(face_id),
1455 None,
1456 symbol,
1457 diameter_symbol,
1458 &tolerance,
1459 &datums,
1460 precision,
1461 frame_position.as_ref(),
1462 frame_plane.id,
1463 leader_scale.as_ref(),
1464 font_size.as_ref(),
1465 exec_state,
1466 args,
1467 &mut annotations,
1468 )
1469 .await?;
1470 }
1471 for edge in &edges {
1472 match edge {
1473 GdtEdgeReference::Entity(edge) => {
1474 create_feature_control_annotation(
1475 Some(edge.get_engine_id(exec_state, args)?),
1476 None,
1477 symbol,
1478 diameter_symbol,
1479 &tolerance,
1480 &datums,
1481 precision,
1482 frame_position.as_ref(),
1483 frame_plane.id,
1484 leader_scale.as_ref(),
1485 font_size.as_ref(),
1486 exec_state,
1487 args,
1488 &mut annotations,
1489 )
1490 .await?;
1491 }
1492 GdtEdgeReference::Specifier(edge_reference) => {
1493 create_feature_control_annotation(
1494 None,
1495 Some(edge_reference.clone()),
1496 symbol,
1497 diameter_symbol,
1498 &tolerance,
1499 &datums,
1500 precision,
1501 frame_position.as_ref(),
1502 frame_plane.id,
1503 leader_scale.as_ref(),
1504 font_size.as_ref(),
1505 exec_state,
1506 args,
1507 &mut annotations,
1508 )
1509 .await?;
1510 }
1511 }
1512 }
1513
1514 Ok(annotations)
1515}
1516
1517#[allow(clippy::too_many_arguments)]
1518async fn create_feature_control_annotation(
1519 entity_id: Option<uuid::Uuid>,
1520 edge_reference: Option<kcmc::shared::EdgeSpecifier>,
1521 symbol: MbdSymbol,
1522 diameter_symbol: Option<MbdSymbol>,
1523 tolerance: &TyF64,
1524 datums: &[char],
1525 precision: u32,
1526 frame_position: Option<&[TyF64; 2]>,
1527 frame_plane_id: uuid::Uuid,
1528 leader_scale: Option<&TyF64>,
1529 font_size: Option<&TyF64>,
1530 exec_state: &mut ExecState,
1531 args: &Args,
1532 annotations: &mut Vec<GdtAnnotation>,
1533) -> Result<(), KclError> {
1534 let meta = vec![Metadata::from(args.source_range)];
1535 let annotation_id = exec_state.next_uuid();
1536 let display_units = exec_state.length_unit();
1537 let control_frame = gdt_control_frame(
1538 symbol,
1539 diameter_symbol,
1540 tolerance.to_length_units(display_units),
1541 datums,
1542 );
1543 let feature_control = AnnotationFeatureControl::builder()
1544 .maybe_entity_id(entity_id)
1545 .maybe_edge_reference(edge_reference)
1546 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1547 .leader_type(AnnotationLineEnd::Dot)
1548 .control_frame(control_frame)
1549 .plane_id(frame_plane_id)
1550 .offset(if let Some(offset) = frame_position {
1551 KPoint2d {
1552 x: offset[0].to_mm(),
1553 y: offset[1].to_mm(),
1554 }
1555 } else {
1556 KPoint2d { x: 100.0, y: 100.0 }
1557 })
1558 .precision(precision)
1559 .font_scale(gdt_font_scale(font_size, args)?)
1560 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1561 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1562 .build();
1563 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1564 exec_state
1565 .batch_modeling_cmd(
1566 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1567 ModelingCmd::from(
1568 mcmd::NewAnnotation::builder()
1569 .options(options)
1570 .clobber(false)
1571 .annotation_type(AnnotationType::T3D)
1572 .build(),
1573 ),
1574 )
1575 .await?;
1576 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1577 annotations.push(GdtAnnotation {
1578 id: annotation_id,
1579 meta,
1580 });
1581 Ok(())
1582}
1583
1584fn gdt_control_frame(
1585 symbol: MbdSymbol,
1586 diameter_symbol: Option<MbdSymbol>,
1587 tolerance: f64,
1588 datums: &[char],
1589) -> AnnotationMbdControlFrame {
1590 match datums {
1591 [] => AnnotationMbdControlFrame::builder()
1592 .symbol(symbol)
1593 .maybe_diameter_symbol(diameter_symbol)
1594 .tolerance(tolerance)
1595 .build(),
1596 [primary] => AnnotationMbdControlFrame::builder()
1597 .symbol(symbol)
1598 .maybe_diameter_symbol(diameter_symbol)
1599 .tolerance(tolerance)
1600 .primary_datum(*primary)
1601 .build(),
1602 [primary, secondary] => AnnotationMbdControlFrame::builder()
1603 .symbol(symbol)
1604 .maybe_diameter_symbol(diameter_symbol)
1605 .tolerance(tolerance)
1606 .primary_datum(*primary)
1607 .secondary_datum(*secondary)
1608 .build(),
1609 [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1610 .symbol(symbol)
1611 .maybe_diameter_symbol(diameter_symbol)
1612 .tolerance(tolerance)
1613 .primary_datum(*primary)
1614 .secondary_datum(*secondary)
1615 .tertiary_datum(*tertiary)
1616 .build(),
1617 _ => unreachable!("resolve_datums rejects more than three datums"),
1618 }
1619}
1620
1621#[allow(clippy::too_many_arguments)]
1622async fn create_annotation(
1623 entity_id: Option<uuid::Uuid>,
1624 edge_reference: Option<kcmc::shared::EdgeSpecifier>,
1625 annotation: &str,
1626 frame_position: Option<&[TyF64; 2]>,
1627 frame_plane_id: uuid::Uuid,
1628 leader_scale: Option<&TyF64>,
1629 font_size: Option<&TyF64>,
1630 exec_state: &mut ExecState,
1631 args: &Args,
1632 annotations: &mut Vec<GdtAnnotation>,
1633) -> Result<(), KclError> {
1634 let meta = vec![Metadata::from(args.source_range)];
1635 let annotation_id = exec_state.next_uuid();
1636 let feature_control = AnnotationFeatureControl::builder()
1637 .maybe_entity_id(entity_id)
1638 .maybe_edge_reference(edge_reference)
1639 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1640 .leader_type(AnnotationLineEnd::Dot)
1641 .prefix(annotation.to_owned())
1642 .plane_id(frame_plane_id)
1643 .offset(if let Some(offset) = frame_position {
1644 KPoint2d {
1645 x: offset[0].to_mm(),
1646 y: offset[1].to_mm(),
1647 }
1648 } else {
1649 KPoint2d { x: 100.0, y: 100.0 }
1650 })
1651 .precision(0)
1652 .font_scale(gdt_font_scale(font_size, args)?)
1653 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1654 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1655 .build();
1656 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1657 exec_state
1658 .batch_modeling_cmd(
1659 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1660 ModelingCmd::from(
1661 mcmd::NewAnnotation::builder()
1662 .options(options)
1663 .clobber(false)
1664 .annotation_type(AnnotationType::T3D)
1665 .build(),
1666 ),
1667 )
1668 .await?;
1669 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1670 annotations.push(GdtAnnotation {
1671 id: annotation_id,
1672 meta,
1673 });
1674 Ok(())
1675}
1676
1677fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1678 let datums = datums.unwrap_or_default();
1679 if datums.len() > 3 {
1680 return Err(KclError::new_semantic(KclErrorDetails::new(
1681 format!("{annotation_name} datums must include at most three names."),
1682 vec![args.source_range],
1683 )));
1684 }
1685
1686 let mut resolved = Vec::with_capacity(datums.len());
1687 for datum in &datums {
1688 let mut chars = datum.chars();
1689 let Some(name) = chars.next() else {
1690 return Err(KclError::new_semantic(KclErrorDetails::new(
1691 format!("{annotation_name} datum names must be a single character."),
1692 vec![args.source_range],
1693 )));
1694 };
1695 if chars.next().is_some() {
1696 return Err(KclError::new_semantic(KclErrorDetails::new(
1697 format!("{annotation_name} datum names must be a single character."),
1698 vec![args.source_range],
1699 )));
1700 }
1701 resolved.push(name);
1702 }
1703
1704 Ok(resolved)
1705}
1706
1707async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1710 let plane_ast = plane_ast("XY", args.source_range);
1711 let metadata = Metadata::from(args.source_range);
1712 let plane_value = args.ctx.eval_expr_fresh_root(&plane_ast, exec_state, &metadata).await?;
1713 let plane_value = plane_value.into_value();
1715 Ok(plane_value
1716 .as_plane()
1717 .ok_or_else(|| {
1718 KclError::new_internal(KclErrorDetails::new(
1719 "Expected XY plane to be defined".to_owned(),
1720 vec![args.source_range],
1721 ))
1722 })?
1723 .clone())
1724}
1725
1726fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1728 ast::Node::new(
1729 ast::Expr::Name(BoxNode::new(ast::Node::new(
1730 ast::Name {
1731 name: ast::Identifier::new(plane_name),
1732 path: Vec::new(),
1733 abs_path: false,
1736 digest: None,
1737 },
1738 range.start(),
1739 range.end(),
1740 range.module_id(),
1741 ))),
1742 range.start(),
1743 range.end(),
1744 range.module_id(),
1745 )
1746}
1747
1748#[cfg(test)]
1749mod tests {
1750 use super::*;
1751 use crate::ExecutorContext;
1752 use crate::execution::Artifact;
1753 use crate::execution::ExecutorSettings;
1754 use crate::execution::MockConfig;
1755 use crate::execution::parse_execute;
1756
1757 const GDT_DISTANCE_KCL_TEMPLATE: &str = r#"
1758@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1759
1760sketch001 = sketch(on = XY) {
1761 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1762 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1763 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1764 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1765 coincident([line1.end, line2.start])
1766 coincident([line2.end, line3.start])
1767 coincident([line3.end, line4.start])
1768 coincident([line4.end, line1.start])
1769 parallel([line2, line4])
1770 parallel([line3, line1])
1771 perpendicular([line1, line2])
1772 horizontal(line3)
1773}
1774
1775region001 = region(point = [5mm, 5mm], sketch = sketch001)
1776extrude001 = extrude(region001, length = 10mm)
1777gdt::distance(
1778 edges = [
1779 getCommonEdge(faces = [
1780 region001.tags.line4,
1781 region001.tags.line1
1782 ])
1783 ],
1784 tolerance = __TOLERANCE__,
1785 framePosition = __FRAME_POSITION__,
1786 fontSize = 2in,
1787)
1788"#;
1789
1790 const GDT_FLATNESS_KCL_TEMPLATE: &str = r#"
1791@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1792
1793sketch001 = sketch(on = XY) {
1794 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1795 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1796 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1797 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1798 coincident([line1.end, line2.start])
1799 coincident([line2.end, line3.start])
1800 coincident([line3.end, line4.start])
1801 coincident([line4.end, line1.start])
1802 parallel([line2, line4])
1803 parallel([line3, line1])
1804 perpendicular([line1, line2])
1805 horizontal(line3)
1806}
1807
1808region001 = region(point = [5mm, 5mm], sketch = sketch001)
1809extrude001 = extrude(region001, length = 10mm, tagEnd = $capEnd001)
1810gdt::flatness(
1811 faces = [capEnd001],
1812 tolerance = __TOLERANCE__,
1813 framePosition = __FRAME_POSITION__,
1814 framePlane = XZ,
1815 fontSize = 2in,
1816)
1817"#;
1818
1819 fn gdt_distance_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1820 GDT_DISTANCE_KCL_TEMPLATE
1821 .replace("__UNIT__", unit)
1822 .replace("__TOLERANCE__", tolerance)
1823 .replace("__FRAME_POSITION__", frame_position)
1824 }
1825
1826 fn gdt_flatness_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1827 GDT_FLATNESS_KCL_TEMPLATE
1828 .replace("__UNIT__", unit)
1829 .replace("__TOLERANCE__", tolerance)
1830 .replace("__FRAME_POSITION__", frame_position)
1831 }
1832
1833 async fn gdt_commands(code: &str) -> Vec<ModelingCmd> {
1834 let result = parse_execute(code).await.unwrap();
1835 result
1836 .root_module_artifact_commands()
1837 .iter()
1838 .map(|artifact_command| artifact_command.command.clone())
1839 .collect()
1840 }
1841
1842 fn annotation_options(command: &ModelingCmd) -> Result<&AnnotationOptions, KclError> {
1843 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1844 return Err(KclError::new_internal(KclErrorDetails::new(
1845 format!("expected new_annotation command, got {command:?}"),
1846 vec![SourceRange::default()],
1847 )));
1848 };
1849 Ok(&new_annotation.options)
1850 }
1851
1852 fn feature_control(command: &ModelingCmd) -> Result<&AnnotationFeatureControl, KclError> {
1853 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1854 return Err(KclError::new_internal(KclErrorDetails::new(
1855 format!("expected new_annotation command, got {command:?}"),
1856 vec![SourceRange::default()],
1857 )));
1858 };
1859 new_annotation.options.feature_control.as_ref().ok_or_else(|| {
1860 KclError::new_internal(KclErrorDetails::new(
1861 "expected new_annotation command to have a feature_control".to_owned(),
1862 vec![SourceRange::default()],
1863 ))
1864 })
1865 }
1866
1867 fn find_control_frame_with_symbol(
1868 commands: &[ModelingCmd],
1869 symbol: MbdSymbol,
1870 ) -> Result<&AnnotationMbdControlFrame, KclError> {
1871 for command in commands {
1872 if let Ok(feature_control) = feature_control(command)
1873 && let Some(control_frame) = feature_control.control_frame.as_ref()
1874 && control_frame.symbol == symbol
1875 {
1876 return Ok(control_frame);
1877 }
1878 }
1879
1880 Err(KclError::new_internal(KclErrorDetails::new(
1881 format!("expected commands to contain a {symbol:?} control frame"),
1882 vec![SourceRange::default()],
1883 )))
1884 }
1885
1886 #[track_caller]
1887 fn assert_close(actual: f64, expected: f64) {
1888 assert!((actual - expected).abs() < 1e-6, "expected {expected}, got {actual}");
1889 }
1890
1891 fn new_annotation_command_index(commands: &[ModelingCmd]) -> Result<usize, KclError> {
1892 commands
1893 .iter()
1894 .position(|command| matches!(command, ModelingCmd::NewAnnotation(_)))
1895 .ok_or_else(|| {
1896 KclError::new_internal(KclErrorDetails::new(
1897 "expected commands to contain a new_annotation command".to_owned(),
1898 vec![SourceRange::default()],
1899 ))
1900 })
1901 }
1902
1903 #[test]
1904 fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1905 let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1906 assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1907
1908 let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1909 assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1910
1911 let inch_in_mm = 25.4;
1912 let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1913 assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1914 }
1915
1916 const GDT_FLATNESS_LEADER_KCL_TEMPLATE: &str = r#"
1917@settings(defaultLengthUnit = mm, kclVersion = 2)
1918
1919blockProfile = sketch(on = XY) {
1920 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1921 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1922 edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1923 edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1924 coincident([edge1.end, edge2.start])
1925 coincident([edge2.end, edge3.start])
1926 coincident([edge3.end, edge4.start])
1927 coincident([edge4.end, edge1.start])
1928 parallel([edge2, edge4])
1929 parallel([edge3, edge1])
1930 perpendicular([edge1, edge2])
1931 horizontal(edge3)
1932}
1933
1934region001 = region(point = [5mm, 5mm], sketch = blockProfile)
1935extrude001 = extrude(region001, length = 10mm, tagEnd = $top)
1936gdt::flatness(
1937 faces = [top],
1938 tolerance = 0.1mm,
1939 framePosition = [10mm, 0mm],
1940 framePlane = XZ,
1941 fontSize = __FONT_SIZE__
1942 __LEADER_SCALE__
1943)
1944"#;
1945
1946 fn gdt_flatness_leader_kcl(font_size: &str, leader_scale: Option<&str>) -> String {
1947 GDT_FLATNESS_LEADER_KCL_TEMPLATE
1948 .replace("__FONT_SIZE__", font_size)
1949 .replace(
1950 "__LEADER_SCALE__",
1951 leader_scale
1952 .map(|scale| format!(",\n leaderScale = {scale}"))
1953 .unwrap_or_default()
1954 .as_str(),
1955 )
1956 }
1957
1958 async fn gdt_flatness_feature_control(
1959 font_size: &str,
1960 leader_scale: Option<&str>,
1961 ) -> Result<AnnotationFeatureControl, KclError> {
1962 let code = gdt_flatness_leader_kcl(font_size, leader_scale);
1963 let commands = gdt_commands(&code).await;
1964 let annotation_index = new_annotation_command_index(&commands)?;
1965 Ok(feature_control(&commands[annotation_index])?.clone())
1966 }
1967
1968 #[tokio::test(flavor = "multi_thread")]
1969 async fn gdt_dot_leader_scale_is_normalized_against_font_scale() -> Result<(), KclError> {
1970 let tiny = gdt_flatness_feature_control("1mm", None).await?;
1971 let large = gdt_flatness_feature_control("100mm", None).await?;
1972
1973 assert_close(f64::from(tiny.font_scale), gdt_font_scale_for_height_mm(1.0).into());
1974 assert_close(f64::from(large.font_scale), gdt_font_scale_for_height_mm(100.0).into());
1975 assert_close(f64::from(tiny.leader_scale), 50.0);
1976 assert_close(f64::from(large.leader_scale), 0.5);
1977
1978 assert_close(
1979 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1980 f64::from(gdt_dot_leader_normal_size()),
1981 );
1982 assert_close(
1983 f64::from(large.font_scale) * f64::from(large.leader_scale),
1984 f64::from(gdt_dot_leader_normal_size()),
1985 );
1986 Ok(())
1987 }
1988
1989 #[tokio::test(flavor = "multi_thread")]
1990 async fn explicit_gdt_dot_leader_scale_multiplies_normal_size() -> Result<(), KclError> {
1991 let tiny = gdt_flatness_feature_control("1mm", Some("2")).await?;
1992 let large = gdt_flatness_feature_control("100mm", Some("2")).await?;
1993
1994 let expected_scaled_dot_size = f64::from(gdt_dot_leader_normal_size()) * 2.0;
1995 assert_close(
1996 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1997 expected_scaled_dot_size,
1998 );
1999 assert_close(
2000 f64::from(large.font_scale) * f64::from(large.leader_scale),
2001 expected_scaled_dot_size,
2002 );
2003 Ok(())
2004 }
2005
2006 #[tokio::test(flavor = "multi_thread")]
2007 async fn gdt_flatness_uses_scene_units_for_control_frame_tolerance() -> Result<(), KclError> {
2008 let cases = [
2009 ("in", "0.1in", "[10, -10]", 0.1, 254.0, -254.0),
2010 ("cm", "10mm", "[1, -1]", 1.0, 10.0, -10.0),
2011 ];
2012
2013 for (default_unit, tolerance, frame_position, expected_tolerance, expected_x, expected_y) in cases {
2014 let code = gdt_flatness_kcl(default_unit, tolerance, frame_position);
2015 let commands = gdt_commands(&code).await;
2016 let annotation_index = new_annotation_command_index(&commands)?;
2017 let feature_control = feature_control(&commands[annotation_index])?;
2018 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
2019 KclError::new_internal(KclErrorDetails::new(
2020 "expected feature_control to have a control_frame".to_owned(),
2021 vec![SourceRange::default()],
2022 ))
2023 })?;
2024
2025 assert_close(control_frame.tolerance, expected_tolerance);
2026 assert_close(feature_control.offset.x, expected_x);
2027 assert_close(feature_control.offset.y, expected_y);
2028 assert_close(
2029 f64::from(feature_control.font_scale),
2030 gdt_font_scale_for_height_mm(50.8).into(),
2031 );
2032 }
2033 Ok(())
2034 }
2035
2036 #[tokio::test(flavor = "multi_thread")]
2037 async fn gdt_distance_sets_units() -> Result<(), KclError> {
2038 let cases = [
2039 (
2040 "in",
2041 "2.54mm",
2042 "[10, -10]",
2043 kcmc::units::UnitLength::Inches,
2044 0.1,
2045 254.0,
2046 -254.0,
2047 ),
2048 (
2049 "cm",
2050 "10mm",
2051 "[1, -1]",
2052 kcmc::units::UnitLength::Centimeters,
2053 1.0,
2054 10.0,
2055 -10.0,
2056 ),
2057 (
2058 "mm",
2059 "2.54mm",
2060 "[10, -10]",
2061 kcmc::units::UnitLength::Millimeters,
2062 2.54,
2063 10.0,
2064 -10.0,
2065 ),
2066 ];
2067
2068 for (default_unit, tolerance, frame_position, scene_unit, expected_tolerance, expected_x, expected_y) in cases {
2069 let code = gdt_distance_kcl(default_unit, tolerance, frame_position);
2070 let commands = gdt_commands(&code).await;
2071 let annotation_index = new_annotation_command_index(&commands)?;
2072 let options = annotation_options(&commands[annotation_index])?;
2073
2074 assert_eq!(options.units, Some(scene_unit));
2075
2076 let dimension = options
2077 .dimension
2078 .as_ref()
2079 .expect("expected new_annotation command to have a dimension");
2080 assert_close(dimension.dimension.tolerance, expected_tolerance);
2081 assert_close(dimension.offset.x, expected_x);
2082 assert_close(dimension.offset.y, expected_y);
2083 assert_close(
2084 f64::from(dimension.font_scale),
2085 gdt_font_scale_for_height_mm(50.8).into(),
2086 );
2087 }
2088 Ok(())
2089 }
2090
2091 const GDT_FACE_API_EDGE_KCL_TEMPLATE: &str = r#"
2092@settings(defaultLengthUnit = mm, kclVersion = 2)
2093
2094sketch001 = sketch(on = XY) {
2095 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2096 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
2097 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
2098 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
2099 coincident([line1.end, line2.start])
2100 coincident([line2.end, line3.start])
2101 coincident([line3.end, line4.start])
2102 coincident([line4.end, line1.start])
2103 parallel([line2, line4])
2104 parallel([line3, line1])
2105 perpendicular([line1, line2])
2106 horizontal(line3)
2107}
2108
2109region001 = region(point = [5mm, 5mm], sketch = sketch001)
2110extrude001 = extrude(region001, length = 10mm, tagStart = $capStart001)
2111__GDT_CALL__
2112"#;
2113
2114 fn gdt_face_api_edge_kcl(gdt_call: &str) -> String {
2115 GDT_FACE_API_EDGE_KCL_TEMPLATE.replace("__GDT_CALL__", gdt_call)
2116 }
2117
2118 fn assert_feature_control_uses_edge_reference(feature_control: &AnnotationFeatureControl) {
2119 assert!(feature_control.entity_id.is_none());
2120 let edge_reference = feature_control
2121 .edge_reference
2122 .as_ref()
2123 .expect("expected face API edge specifier to emit edge_reference");
2124 assert_eq!(edge_reference.side_faces.len(), 2);
2125 assert!(edge_reference.end_faces.is_empty());
2126 assert_eq!(edge_reference.index, None);
2127 }
2128
2129 #[tokio::test(flavor = "multi_thread")]
2130 async fn gdt_straightness_accepts_face_api_edge_specifier() -> Result<(), KclError> {
2131 let code = gdt_face_api_edge_kcl(
2132 r#"gdt::straightness(
2133 edges = [
2134 {
2135 sideFaces = [region001.tags.line1, capStart001]
2136 }
2137 ],
2138 tolerance = 0.1mm,
2139 framePosition = [12mm, 8mm],
2140 framePlane = XZ,
2141)"#,
2142 );
2143 let commands = gdt_commands(&code).await;
2144 let annotation_index = new_annotation_command_index(&commands)?;
2145 let feature_control = feature_control(&commands[annotation_index])?;
2146 assert_feature_control_uses_edge_reference(feature_control);
2147 assert!(feature_control.control_frame.is_some());
2148 Ok(())
2149 }
2150
2151 #[tokio::test(flavor = "multi_thread")]
2152 async fn gdt_annotation_accepts_face_api_edge_specifier() -> Result<(), KclError> {
2153 let code = gdt_face_api_edge_kcl(
2154 r#"gdt::annotation(
2155 annotation = "A",
2156 edges = [
2157 {
2158 sideFaces = [region001.tags.line1, capStart001]
2159 }
2160 ],
2161 framePosition = [12mm, 8mm],
2162 framePlane = XZ,
2163)"#,
2164 );
2165 let commands = gdt_commands(&code).await;
2166 let annotation_index = new_annotation_command_index(&commands)?;
2167 let feature_control = feature_control(&commands[annotation_index])?;
2168 assert_feature_control_uses_edge_reference(feature_control);
2169 assert_eq!(feature_control.prefix.as_deref(), Some("A"));
2170 Ok(())
2171 }
2172
2173 #[tokio::test(flavor = "multi_thread")]
2174 async fn gdt_distance_accepts_face_api_edge_specifier() -> Result<(), KclError> {
2175 let code = gdt_face_api_edge_kcl(
2176 r#"gdt::distance(
2177 edges = [
2178 {
2179 sideFaces = [region001.tags.line1, capStart001]
2180 }
2181 ],
2182 tolerance = 0.1mm,
2183 framePosition = [12mm, 8mm],
2184 framePlane = XZ,
2185)"#,
2186 );
2187 let commands = gdt_commands(&code).await;
2188 let annotation_index = new_annotation_command_index(&commands)?;
2189 let options = annotation_options(&commands[annotation_index])?;
2190 let dimension = options
2191 .dimension
2192 .as_ref()
2193 .expect("expected new_annotation command to have a dimension");
2194 assert!(dimension.from_entity_id.is_none());
2195 assert!(dimension.to_entity_id.is_none());
2196 assert_eq!(
2197 dimension
2198 .from_edge_reference
2199 .as_ref()
2200 .expect("expected from_edge_reference")
2201 .side_faces
2202 .len(),
2203 2
2204 );
2205 assert_eq!(
2206 dimension
2207 .to_edge_reference
2208 .as_ref()
2209 .expect("expected to_edge_reference")
2210 .side_faces
2211 .len(),
2212 2
2213 );
2214 Ok(())
2215 }
2216
2217 #[tokio::test(flavor = "multi_thread")]
2218 async fn gdt_distance_from_to_accept_face_api_edge_specifiers() -> Result<(), KclError> {
2219 let code = gdt_face_api_edge_kcl(
2220 r#"gdt::distance(
2221 from = {
2222 sideFaces = [region001.tags.line1, capStart001]
2223 },
2224 to = {
2225 sideFaces = [region001.tags.line3, capStart001]
2226 },
2227 tolerance = 0.1mm,
2228 framePosition = [12mm, 8mm],
2229 framePlane = XZ,
2230)"#,
2231 );
2232 let commands = gdt_commands(&code).await;
2233 let annotation_index = new_annotation_command_index(&commands)?;
2234 let options = annotation_options(&commands[annotation_index])?;
2235 let dimension = options
2236 .dimension
2237 .as_ref()
2238 .expect("expected new_annotation command to have a dimension");
2239 assert!(dimension.from_entity_id.is_none());
2240 assert!(dimension.to_entity_id.is_none());
2241 assert_eq!(
2242 dimension
2243 .from_edge_reference
2244 .as_ref()
2245 .expect("expected from_edge_reference")
2246 .side_faces
2247 .len(),
2248 2
2249 );
2250 assert_eq!(
2251 dimension
2252 .to_edge_reference
2253 .as_ref()
2254 .expect("expected to_edge_reference")
2255 .side_faces
2256 .len(),
2257 2
2258 );
2259 Ok(())
2260 }
2261
2262 const GDT_DATUM_KCL: &str = r#"
2263blockProfile = sketch(on = XY) {
2264 edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
2265 edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
2266 edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
2267 edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
2268 coincident([edge1.end, edge2.start])
2269 coincident([edge2.end, edge3.start])
2270 coincident([edge3.end, edge4.start])
2271 coincident([edge4.end, edge1.start])
2272 horizontal(edge1)
2273 vertical(edge2)
2274 horizontal(edge3)
2275 vertical(edge4)
2276}
2277
2278block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2279
2280gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
2281"#;
2282
2283 async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
2284 let settings = ExecutorSettings {
2285 skip_artifact_graph,
2286 ..Default::default()
2287 };
2288 let ctx = ExecutorContext::new_mock(Some(settings)).await;
2289 let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
2290 let mock_config = MockConfig {
2291 use_prev_memory: false,
2292 ..Default::default()
2293 };
2294 let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
2295 ctx.close().await;
2296
2297 outcome
2298 .artifact_graph
2299 .values()
2300 .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
2301 .count()
2302 }
2303
2304 #[tokio::test(flavor = "multi_thread")]
2305 async fn gdt_annotations_do_not_follow_runtime_artifact_graph_setting() {
2306 assert_eq!(gdt_artifact_count(false).await, 1);
2307 assert_eq!(gdt_artifact_count(true).await, 1);
2308 }
2309
2310 const GDT_ANGULARITY_FACE_KCL: &str = r#"
2311@settings(defaultLengthUnit = mm, kclVersion = 2)
2312
2313basicAngle = 30deg
2314thickness = 3.5mm
2315flangeLength = 24mm
2316bendStartX = 5mm
2317legLength = 30mm
2318legRun = legLength * cos(basicAngle)
2319legRise = legLength * sin(basicAngle)
2320normalRun = thickness * sin(basicAngle)
2321normalRise = thickness * cos(basicAngle)
2322annotationFont = 2mm
2323
2324stampedProfile = sketch(on = XY) {
2325 datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
2326 flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
2327 innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
2328 controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
2329 tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
2330 outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
2331 outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
2332 coincident([datumFace.end, flangeEnd.start])
2333 coincident([flangeEnd.end, innerFlange.start])
2334 coincident([innerFlange.end, controlledSurface.start])
2335 coincident([controlledSurface.end, tabEnd.start])
2336 coincident([tabEnd.end, outerSurface.start])
2337 coincident([outerSurface.end, outsideBend.start])
2338 coincident([outsideBend.end, datumFace.start])
2339 coincident([datumFace.start, ORIGIN])
2340 horizontal(datumFace)
2341 horizontal(innerFlange)
2342 vertical(flangeEnd)
2343 distance([datumFace.start, datumFace.end]) == flangeLength
2344 distance([flangeEnd.start, flangeEnd.end]) == thickness
2345 distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
2346 distance([controlledSurface.start, controlledSurface.end]) == legLength
2347 distance([tabEnd.start, tabEnd.end]) == thickness
2348 distance([outerSurface.start, outerSurface.end]) == legLength
2349 parallel([controlledSurface, outerSurface])
2350 perpendicular([controlledSurface, tabEnd])
2351 angle([datumFace, controlledSurface]) == basicAngle
2352}
2353
2354stampedPart = extrude(region(point = [12mm, 2mm], sketch = stampedProfile), length = 0.8mm)
2355
2356gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
2357gdt::angularity(faces = [stampedPart.sketch.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
2358"#;
2359
2360 const GDT_ANGULARITY_EDGE_KCL: &str = r#"
2361@settings(defaultLengthUnit = mm, kclVersion = 2)
2362
2363basicAngle = 30deg
2364thickness = 3.5mm
2365flangeLength = 24mm
2366bendStartX = 5mm
2367legLength = 30mm
2368legRun = legLength * cos(basicAngle)
2369legRise = legLength * sin(basicAngle)
2370normalRun = thickness * sin(basicAngle)
2371normalRise = thickness * cos(basicAngle)
2372annotationFont = 2mm
2373
2374stampedProfile = sketch(on = XY) {
2375 datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
2376 flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
2377 innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
2378 controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
2379 tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
2380 outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
2381 outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
2382 coincident([datumFace.end, flangeEnd.start])
2383 coincident([flangeEnd.end, innerFlange.start])
2384 coincident([innerFlange.end, controlledSurface.start])
2385 coincident([controlledSurface.end, tabEnd.start])
2386 coincident([tabEnd.end, outerSurface.start])
2387 coincident([outerSurface.end, outsideBend.start])
2388 coincident([outsideBend.end, datumFace.start])
2389 coincident([datumFace.start, ORIGIN])
2390 horizontal(datumFace)
2391 horizontal(innerFlange)
2392 vertical(flangeEnd)
2393 distance([datumFace.start, datumFace.end]) == flangeLength
2394 distance([flangeEnd.start, flangeEnd.end]) == thickness
2395 distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
2396 distance([controlledSurface.start, controlledSurface.end]) == legLength
2397 distance([tabEnd.start, tabEnd.end]) == thickness
2398 distance([outerSurface.start, outerSurface.end]) == legLength
2399 parallel([controlledSurface, outerSurface])
2400 perpendicular([controlledSurface, tabEnd])
2401 angle([datumFace, controlledSurface]) == basicAngle
2402}
2403
2404stampedRegion = region(point = [12mm, 2mm], sketch = stampedProfile)
2405hide(stampedProfile)
2406stampedPart = extrude(stampedRegion, length = 0.8mm)
2407
2408gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
2409gdt::angularity(edges = [stampedRegion.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
2410"#;
2411
2412 #[tokio::test(flavor = "multi_thread")]
2413 async fn gdt_angularity_uses_angularity_symbol_with_datums() -> Result<(), KclError> {
2414 let cases = [
2415 ("angled face", GDT_ANGULARITY_FACE_KCL, 0.1),
2416 ("angled edge", GDT_ANGULARITY_EDGE_KCL, 0.1),
2417 ];
2418
2419 for (label, code, expected_tolerance) in cases {
2420 let commands = gdt_commands(code).await;
2421 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Angularity)?;
2422
2423 assert_close(control_frame.tolerance, expected_tolerance);
2424 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
2425 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2426 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2427 }
2428 Ok(())
2429 }
2430
2431 const GDT_PROFILE_LINE_KCL: &str = r#"
2432@settings(defaultLengthUnit = mm, kclVersion = 2)
2433
2434blockProfile = sketch(on = XY) {
2435 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2436 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 6mm])
2437 edge3 = line(start = [var 10mm, var 6mm], end = [var 0mm, var 6mm])
2438 edge4 = line(start = [var 0mm, var 6mm], end = [var 0mm, var 0mm])
2439 coincident([edge1.end, edge2.start])
2440 coincident([edge2.end, edge3.start])
2441 coincident([edge3.end, edge4.start])
2442 coincident([edge4.end, edge1.start])
2443 horizontal(edge1)
2444 vertical(edge2)
2445 horizontal(edge3)
2446 vertical(edge4)
2447}
2448
2449block = extrude(region(point = [5mm, 3mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2450profileEdge = getCommonEdge(faces = [block.sketch.tags.edge1, top])
2451gdt::profileLine(edges = [profileEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2452"#;
2453
2454 const GDT_PROFILE_GENERIC_LINE_KCL: &str = r#"
2455@settings(defaultLengthUnit = mm, kclVersion = 2)
2456
2457blockProfile = sketch(on = XY) {
2458 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2459 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 6mm])
2460 edge3 = line(start = [var 10mm, var 6mm], end = [var 0mm, var 6mm])
2461 edge4 = line(start = [var 0mm, var 6mm], end = [var 0mm, var 0mm])
2462 coincident([edge1.end, edge2.start])
2463 coincident([edge2.end, edge3.start])
2464 coincident([edge3.end, edge4.start])
2465 coincident([edge4.end, edge1.start])
2466 horizontal(edge1)
2467 vertical(edge2)
2468 horizontal(edge3)
2469 vertical(edge4)
2470}
2471
2472block = extrude(region(point = [5mm, 3mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2473profileEdge = getCommonEdge(faces = [block.sketch.tags.edge1, top])
2474gdt::profile(edges = [profileEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2475"#;
2476
2477 const GDT_PROFILE_SURFACE_KCL: &str = r#"
2478@settings(defaultLengthUnit = mm, kclVersion = 2)
2479
2480cylinderSketch = sketch(on = XY) {
2481 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2482}
2483
2484cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2485gdt::profileSurface(faces = [top], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2486"#;
2487
2488 const GDT_PROFILE_GENERIC_SURFACE_KCL: &str = r#"
2489@settings(defaultLengthUnit = mm, kclVersion = 2)
2490
2491cylinderSketch = sketch(on = XY) {
2492 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2493}
2494
2495cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2496gdt::profile(faces = [top], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2497"#;
2498
2499 const GDT_PROFILE_BOTH_KCL: &str = r#"
2500@settings(defaultLengthUnit = mm, kclVersion = 2)
2501
2502blockProfile = sketch(on = XY) {
2503 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2504 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 6mm])
2505 edge3 = line(start = [var 10mm, var 6mm], end = [var 0mm, var 6mm])
2506 edge4 = line(start = [var 0mm, var 6mm], end = [var 0mm, var 0mm])
2507 coincident([edge1.end, edge2.start])
2508 coincident([edge2.end, edge3.start])
2509 coincident([edge3.end, edge4.start])
2510 coincident([edge4.end, edge1.start])
2511 horizontal(edge1)
2512 vertical(edge2)
2513 horizontal(edge3)
2514 vertical(edge4)
2515}
2516
2517block = extrude(region(point = [5mm, 3mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2518profileEdge = getCommonEdge(faces = [block.sketch.tags.edge1, top])
2519gdt::profile(edges = [profileEdge], faces = [top], tolerance = 0.05mm)
2520"#;
2521
2522 const GDT_PROFILE_MISSING_ENTITIES_KCL: &str = r#"
2523@settings(defaultLengthUnit = mm, kclVersion = 2)
2524
2525gdt::profile(tolerance = 0.05mm)
2526"#;
2527
2528 #[tokio::test(flavor = "multi_thread")]
2529 async fn gdt_profile_line_uses_profile_of_line_symbol() -> Result<(), KclError> {
2530 let cases = [
2531 ("specific profileLine", GDT_PROFILE_LINE_KCL),
2532 ("generic profile with edges", GDT_PROFILE_GENERIC_LINE_KCL),
2533 ];
2534
2535 for (label, code) in cases {
2536 let commands = gdt_commands(code).await;
2537 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::ProfileOfLine)?;
2538
2539 assert_close(control_frame.tolerance, 0.05);
2540 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2541 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2542 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2543 }
2544 Ok(())
2545 }
2546
2547 #[tokio::test(flavor = "multi_thread")]
2548 async fn gdt_profile_surface_uses_surface_profile_symbol() -> Result<(), KclError> {
2549 let cases = [
2550 ("specific profileSurface", GDT_PROFILE_SURFACE_KCL),
2551 ("generic profile with faces", GDT_PROFILE_GENERIC_SURFACE_KCL),
2552 ];
2553
2554 for (label, code) in cases {
2555 let commands = gdt_commands(code).await;
2556 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::SurfaceProfile)?;
2557
2558 assert_close(control_frame.tolerance, 0.05);
2559 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2560 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2561 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2562 }
2563 Ok(())
2564 }
2565
2566 #[tokio::test(flavor = "multi_thread")]
2567 async fn gdt_profile_requires_edges_or_faces() {
2568 assert_eq!(
2569 parse_execute(GDT_PROFILE_MISSING_ENTITIES_KCL)
2570 .await
2571 .unwrap_err()
2572 .message(),
2573 "Profile requires either `edges` for `profileLine` or `faces` for `profileSurface`.",
2574 );
2575 }
2576
2577 #[tokio::test(flavor = "multi_thread")]
2578 async fn gdt_profile_rejects_combined_edges_and_faces() {
2579 assert_eq!(
2580 parse_execute(GDT_PROFILE_BOTH_KCL).await.unwrap_err().message(),
2581 "Profile cannot combine `edges` and `faces`. Use `profileLine` for edges or `profileSurface` for faces.",
2582 );
2583 }
2584
2585 const GDT_CIRCULARITY_EDGE_KCL: &str = r#"
2591@settings(defaultLengthUnit = mm, kclVersion = 2)
2592
2593cylinderSketch = sketch(on = XY) {
2594 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2595}
2596
2597cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
2598hide(cylinderSketch)
2599cylinder = extrude(cylinderRegion, length = 10mm)
2600gdt::circularity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm)
2601"#;
2602
2603 const GDT_CIRCULARITY_WALL_KCL: &str = r#"
2604@settings(defaultLengthUnit = mm, kclVersion = 2)
2605
2606cylinderSketch = sketch(on = XY) {
2607 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2608}
2609
2610cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
2611gdt::circularity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [12mm, 8mm], framePlane = XZ)
2612"#;
2613
2614 const GDT_CIRCULARITY_COMMON_EDGE_KCL: &str = r#"
2615@settings(defaultLengthUnit = mm, kclVersion = 2)
2616
2617cylinderSketch = sketch(on = XY) {
2618 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2619}
2620
2621cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2622topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
2623gdt::circularity(edges = [topEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2624"#;
2625
2626 #[tokio::test(flavor = "multi_thread")]
2627 async fn gdt_circularity_uses_roundness_symbol_without_datums() -> Result<(), KclError> {
2628 let cases = [
2629 ("circular edge", GDT_CIRCULARITY_EDGE_KCL, 0.05),
2630 ("cylinder wall", GDT_CIRCULARITY_WALL_KCL, 0.02),
2631 ("common edge", GDT_CIRCULARITY_COMMON_EDGE_KCL, 0.05),
2632 ];
2633
2634 for (label, code, expected_tolerance) in cases {
2635 let commands = gdt_commands(code).await;
2636 let annotation_index = new_annotation_command_index(&commands)?;
2637 let feature_control = feature_control(&commands[annotation_index])?;
2638 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
2639 KclError::new_internal(KclErrorDetails::new(
2640 format!("expected {label} feature_control to have a control_frame"),
2641 vec![SourceRange::default()],
2642 ))
2643 })?;
2644
2645 assert_eq!(control_frame.symbol, MbdSymbol::Roundness, "case: {label}");
2646 assert_close(control_frame.tolerance, expected_tolerance);
2647 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2649 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2650 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2651 }
2652 Ok(())
2653 }
2654
2655 const GDT_CYLINDRICITY_WALL_KCL: &str = r#"
2661@settings(defaultLengthUnit = mm, kclVersion = 2)
2662
2663cylinderSketch = sketch(on = XY) {
2664 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2665}
2666
2667cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
2668gdt::cylindricity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [-12mm, 8mm], framePlane = XZ)
2669"#;
2670
2671 const GDT_CYLINDRICITY_EDGE_KCL: &str = r#"
2672@settings(defaultLengthUnit = mm, kclVersion = 2)
2673
2674cylinderSketch = sketch(on = XY) {
2675 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2676}
2677
2678cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
2679hide(cylinderSketch)
2680cylinder = extrude(cylinderRegion, length = 10mm)
2681gdt::cylindricity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm, framePosition = [-12mm, 8mm])
2682"#;
2683
2684 const GDT_CYLINDRICITY_COMMON_EDGE_KCL: &str = r#"
2685@settings(defaultLengthUnit = mm, kclVersion = 2)
2686
2687cylinderSketch = sketch(on = XY) {
2688 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2689}
2690
2691cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2692topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
2693gdt::cylindricity(edges = [topEdge], tolerance = 0.05mm, framePosition = [-12mm, 8mm], framePlane = XZ)
2694"#;
2695
2696 #[tokio::test(flavor = "multi_thread")]
2697 async fn gdt_cylindricity_uses_cylindricity_symbol_without_datums() -> Result<(), KclError> {
2698 let cases = [
2699 ("cylinder wall", GDT_CYLINDRICITY_WALL_KCL, 0.02),
2700 ("circular edge", GDT_CYLINDRICITY_EDGE_KCL, 0.05),
2701 ("common edge", GDT_CYLINDRICITY_COMMON_EDGE_KCL, 0.05),
2702 ];
2703
2704 for (label, code, expected_tolerance) in cases {
2705 let commands = gdt_commands(code).await;
2706 let annotation_index = new_annotation_command_index(&commands)?;
2707 let feature_control = feature_control(&commands[annotation_index])?;
2708 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
2709 KclError::new_internal(KclErrorDetails::new(
2710 format!("expected {label} feature_control to have a control_frame"),
2711 vec![SourceRange::default()],
2712 ))
2713 })?;
2714
2715 assert_eq!(control_frame.symbol, MbdSymbol::Cylindricity, "case: {label}");
2716 assert_close(control_frame.tolerance, expected_tolerance);
2717 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2719 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2720 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2721 }
2722 Ok(())
2723 }
2724
2725 const GDT_CONCENTRICITY_REFERENCE_FEATURE_B_FACE_KCL: &str = r#"
2730@settings(defaultLengthUnit = mm, kclVersion = 2)
2731
2732datumASketch = sketch(on = XY) {
2733 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2734}
2735
2736datumA = extrude(region(point = datumASketch.perimeter.center, sketch = datumASketch), length = 16mm)
2737
2738referenceFeatureBSketch = sketch(on = XY) {
2739 perimeter = circle(start = [var 2.5mm, var 0mm], center = [var 0mm, var 0mm])
2740}
2741
2742referenceFeatureB = extrude(region(point = referenceFeatureBSketch.perimeter.center, sketch = referenceFeatureBSketch), length = 12mm)
2743 |> translate(z = -12mm)
2744
2745gdt::datum(face = datumA.sketch.tags.perimeter, name = "A", framePosition = [10mm, -12mm], framePlane = XZ)
2746gdt::concentricity(faces = [referenceFeatureB.sketch.tags.perimeter], tolerance = 0.2mm, datums = ["A"], framePosition = [-18mm, 12mm], framePlane = XZ)
2747"#;
2748
2749 const GDT_CONCENTRICITY_REFERENCE_FEATURE_B_EDGE_KCL: &str = r#"
2750@settings(defaultLengthUnit = mm, kclVersion = 2)
2751
2752datumASketch = sketch(on = XY) {
2753 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2754}
2755
2756datumA = extrude(region(point = datumASketch.perimeter.center, sketch = datumASketch), length = 16mm)
2757
2758referenceFeatureBSketch = sketch(on = XY) {
2759 perimeter = circle(start = [var 2.5mm, var 0mm], center = [var 0mm, var 0mm])
2760}
2761
2762referenceFeatureB = extrude(region(point = referenceFeatureBSketch.perimeter.center, sketch = referenceFeatureBSketch), length = 12mm, tagEnd = $endB)
2763 |> translate(z = -12mm)
2764endEdgeB = getCommonEdge(faces = [referenceFeatureB.sketch.tags.perimeter, endB])
2765
2766gdt::datum(face = datumA.sketch.tags.perimeter, name = "A", framePosition = [10mm, -12mm], framePlane = XZ)
2767gdt::concentricity(edges = [endEdgeB], tolerance = 0.2mm, datums = ["A"], framePosition = [-18mm, 12mm], framePlane = XZ)
2768"#;
2769
2770 #[tokio::test(flavor = "multi_thread")]
2771 async fn gdt_concentricity_uses_concentricity_symbol_with_diameter_zone_and_datums() -> Result<(), KclError> {
2772 let cases = [
2773 (
2774 "reference feature B face",
2775 GDT_CONCENTRICITY_REFERENCE_FEATURE_B_FACE_KCL,
2776 0.2,
2777 ),
2778 (
2779 "reference feature B edge",
2780 GDT_CONCENTRICITY_REFERENCE_FEATURE_B_EDGE_KCL,
2781 0.2,
2782 ),
2783 ];
2784
2785 for (label, code, expected_tolerance) in cases {
2786 let commands = gdt_commands(code).await;
2787 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Concentricity)?;
2788
2789 assert_eq!(
2790 control_frame.diameter_symbol,
2791 Some(MbdSymbol::Diameter),
2792 "case: {label}"
2793 );
2794 assert_close(control_frame.tolerance, expected_tolerance);
2795 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
2796 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2797 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2798 }
2799 Ok(())
2800 }
2801
2802 const GDT_SYMMETRY_LATCH_BLOCK_GROOVE_FACE_KCL: &str = r#"
2806@settings(defaultLengthUnit = mm, kclVersion = 2)
2807
2808latchProfile = sketch(on = XZ) {
2809 bottom = line(start = [var -20mm, var -10mm], end = [var 20mm, var -10mm])
2810 datumWidthFace = line(start = [var 20mm, var -10mm], end = [var 20mm, var 10mm])
2811 topRight = line(start = [var 20mm, var 10mm], end = [var 5mm, var 10mm])
2812 rightGrooveWall = line(start = [var 5mm, var 10mm], end = [var 5mm, var 3mm])
2813 grooveFloor = line(start = [var 5mm, var 3mm], end = [var -5mm, var 3mm])
2814 leftGrooveWall = line(start = [var -5mm, var 3mm], end = [var -5mm, var 10mm])
2815 topLeft = line(start = [var -5mm, var 10mm], end = [var -20mm, var 10mm])
2816 leftSide = line(start = [var -20mm, var 10mm], end = [var -20mm, var -10mm])
2817 coincident([bottom.end, datumWidthFace.start])
2818 coincident([datumWidthFace.end, topRight.start])
2819 coincident([topRight.end, rightGrooveWall.start])
2820 coincident([rightGrooveWall.end, grooveFloor.start])
2821 coincident([grooveFloor.end, leftGrooveWall.start])
2822 coincident([leftGrooveWall.end, topLeft.start])
2823 coincident([topLeft.end, leftSide.start])
2824 coincident([leftSide.end, bottom.start])
2825 horizontal(bottom)
2826 vertical(datumWidthFace)
2827 horizontal(topRight)
2828 vertical(rightGrooveWall)
2829 horizontal(grooveFloor)
2830 vertical(leftGrooveWall)
2831 horizontal(topLeft)
2832 vertical(leftSide)
2833}
2834
2835latchBlockRegion = region(point = [0mm, 0mm], sketch = latchProfile)
2836latchBlock = extrude(latchBlockRegion, length = 12mm)
2837
2838gdt::datum(face = latchBlock.sketch.tags.bottom, name = "A", framePosition = [0mm, -16mm], framePlane = XZ)
2839gdt::symmetry(faces = [latchBlock.sketch.tags.grooveFloor], tolerance = 0.2mm, datums = ["A"], framePosition = [-24mm, 14mm], framePlane = XZ)
2840"#;
2841
2842 const GDT_SYMMETRY_LATCH_BLOCK_GROOVE_EDGE_KCL: &str = r#"
2843@settings(defaultLengthUnit = mm, kclVersion = 2)
2844
2845latchProfile = sketch(on = XZ) {
2846 bottom = line(start = [var -20mm, var -10mm], end = [var 20mm, var -10mm])
2847 datumWidthFace = line(start = [var 20mm, var -10mm], end = [var 20mm, var 10mm])
2848 topRight = line(start = [var 20mm, var 10mm], end = [var 5mm, var 10mm])
2849 rightGrooveWall = line(start = [var 5mm, var 10mm], end = [var 5mm, var 3mm])
2850 grooveFloor = line(start = [var 5mm, var 3mm], end = [var -5mm, var 3mm])
2851 leftGrooveWall = line(start = [var -5mm, var 3mm], end = [var -5mm, var 10mm])
2852 topLeft = line(start = [var -5mm, var 10mm], end = [var -20mm, var 10mm])
2853 leftSide = line(start = [var -20mm, var 10mm], end = [var -20mm, var -10mm])
2854 coincident([bottom.end, datumWidthFace.start])
2855 coincident([datumWidthFace.end, topRight.start])
2856 coincident([topRight.end, rightGrooveWall.start])
2857 coincident([rightGrooveWall.end, grooveFloor.start])
2858 coincident([grooveFloor.end, leftGrooveWall.start])
2859 coincident([leftGrooveWall.end, topLeft.start])
2860 coincident([topLeft.end, leftSide.start])
2861 coincident([leftSide.end, bottom.start])
2862 horizontal(bottom)
2863 vertical(datumWidthFace)
2864 horizontal(topRight)
2865 vertical(rightGrooveWall)
2866 horizontal(grooveFloor)
2867 vertical(leftGrooveWall)
2868 horizontal(topLeft)
2869 vertical(leftSide)
2870}
2871
2872latchBlockRegion = region(point = [0mm, 0mm], sketch = latchProfile)
2873latchBlock = extrude(latchBlockRegion, length = 12mm, tagEnd = $frontFace)
2874grooveFloorFrontEdge = getCommonEdge(faces = [latchBlock.sketch.tags.grooveFloor, frontFace])
2875
2876gdt::datum(face = latchBlock.sketch.tags.bottom, name = "A", framePosition = [0mm, -16mm], framePlane = XZ)
2877gdt::symmetry(edges = [grooveFloorFrontEdge], tolerance = 0.2mm, datums = ["A"], framePosition = [-24mm, 14mm], framePlane = XZ)
2878"#;
2879
2880 #[tokio::test(flavor = "multi_thread")]
2881 async fn gdt_symmetry_uses_symmetry_symbol_with_datums_for_face() -> Result<(), KclError> {
2882 let commands = gdt_commands(GDT_SYMMETRY_LATCH_BLOCK_GROOVE_FACE_KCL).await;
2883 let control_frames: Vec<_> = commands
2884 .iter()
2885 .filter_map(|command| {
2886 feature_control(command)
2887 .ok()
2888 .and_then(|feature_control| feature_control.control_frame.as_ref())
2889 .filter(|control_frame| control_frame.symbol == MbdSymbol::Symmetry)
2890 })
2891 .collect();
2892
2893 assert_eq!(control_frames.len(), 1);
2894 let control_frame = control_frames[0];
2895 assert_eq!(control_frame.diameter_symbol, None);
2896 assert_close(control_frame.tolerance, 0.2);
2897 assert_eq!(control_frame.primary_datum, Some('A'));
2898 assert!(control_frame.secondary_datum.is_none());
2899 assert!(control_frame.tertiary_datum.is_none());
2900 Ok(())
2901 }
2902
2903 #[tokio::test(flavor = "multi_thread")]
2904 async fn gdt_symmetry_uses_symmetry_symbol_with_datums_for_edge() -> Result<(), KclError> {
2905 let commands = gdt_commands(GDT_SYMMETRY_LATCH_BLOCK_GROOVE_EDGE_KCL).await;
2906 let control_frames: Vec<_> = commands
2907 .iter()
2908 .filter_map(|command| {
2909 feature_control(command)
2910 .ok()
2911 .and_then(|feature_control| feature_control.control_frame.as_ref())
2912 .filter(|control_frame| control_frame.symbol == MbdSymbol::Symmetry)
2913 })
2914 .collect();
2915
2916 assert_eq!(control_frames.len(), 1);
2917 let control_frame = control_frames[0];
2918 assert_eq!(control_frame.diameter_symbol, None);
2919 assert_close(control_frame.tolerance, 0.2);
2920 assert_eq!(control_frame.primary_datum, Some('A'));
2921 assert!(control_frame.secondary_datum.is_none());
2922 assert!(control_frame.tertiary_datum.is_none());
2923 Ok(())
2924 }
2925
2926 const GDT_RUNOUT_STEPPED_SHAFT_KCL: &str = r#"
2930@settings(defaultLengthUnit = mm, kclVersion = 2)
2931
2932annotationPlane = offsetPlane(XZ, offset = 24mm)
2933
2934controlledSketch = sketch(on = YZ) {
2935 upperPerimeter = arc(start = [var 10mm, var 0mm], end = [var -10mm, var 0mm], center = [var 0mm, var 0mm])
2936 lowerPerimeter = arc(start = [var -10mm, var 0mm], end = [var 10mm, var 0mm], center = [var 0mm, var 0mm])
2937 coincident([upperPerimeter.end, lowerPerimeter.start])
2938 coincident([lowerPerimeter.end, upperPerimeter.start])
2939}
2940
2941controlledShaft = extrude(
2942 region(point = [0mm, 1mm], sketch = controlledSketch),
2943 length = -58mm,
2944 tagStart = $controlledShoulder,
2945 tagEnd = $controlledFreeEnd
2946)
2947
2948controlledUpperShoulderEdge = getCommonEdge(faces = [
2949 controlledShaft.sketch.tags.upperPerimeter,
2950 controlledShoulder
2951])
2952
2953datumSketch = sketch(on = YZ) {
2954 perimeter = circle(start = [var 18mm, var 0mm], center = [var 0mm, var 0mm])
2955}
2956
2957datumShaft = extrude(
2958 region(point = datumSketch.perimeter.center, sketch = datumSketch),
2959 length = 36mm,
2960 tagEnd = $datumEnd
2961)
2962
2963gdt::datum(
2964 face = datumShaft.sketch.tags.perimeter,
2965 name = "A",
2966 framePosition = [18mm, -28mm],
2967 framePlane = annotationPlane,
2968 leaderScale = 1.15,
2969 fontSize = 6mm
2970)
2971
2972gdt::runout(
2973 edges = [controlledUpperShoulderEdge],
2974 tolerance = 0.2mm,
2975 datums = ["A"],
2976 precision = 1,
2977 framePosition = [12mm, 48mm],
2978 framePlane = annotationPlane,
2979 leaderScale = 1.15,
2980 fontSize = 6mm
2981)
2982"#;
2983
2984 const GDT_RUNOUT_FACE_KCL: &str = r#"
2985@settings(defaultLengthUnit = mm, kclVersion = 2)
2986
2987datumSketch = sketch(on = XY) {
2988 perimeter = circle(start = [var 6mm, var 0mm], center = [var 0mm, var 0mm])
2989}
2990
2991datumShaft = extrude(region(point = datumSketch.perimeter.center, sketch = datumSketch), length = 18mm)
2992
2993controlledSketch = sketch(on = XY) {
2994 perimeter = circle(start = [var 3mm, var 0mm], center = [var 0mm, var 0mm])
2995}
2996
2997controlledShaft = extrude(region(point = controlledSketch.perimeter.center, sketch = controlledSketch), length = 16mm)
2998 |> translate(z = -16mm)
2999
3000gdt::datum(face = datumShaft.sketch.tags.perimeter, name = "A", framePosition = [12mm, -14mm], framePlane = XZ)
3001gdt::runout(faces = [controlledShaft.sketch.tags.perimeter], tolerance = 0.2mm, datums = ["A"], framePosition = [-18mm, 12mm], framePlane = XZ)
3002"#;
3003
3004 #[tokio::test(flavor = "multi_thread")]
3005 async fn gdt_runout_uses_runout_symbol_with_axis_datum() -> Result<(), KclError> {
3006 let cases = [
3007 ("stepped shaft", GDT_RUNOUT_STEPPED_SHAFT_KCL, 0.2),
3008 ("controlled face", GDT_RUNOUT_FACE_KCL, 0.2),
3009 ];
3010
3011 for (label, code, expected_tolerance) in cases {
3012 let commands = gdt_commands(code).await;
3013 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Runout)?;
3014
3015 assert!(control_frame.diameter_symbol.is_none(), "case: {label}");
3016 assert_close(control_frame.tolerance, expected_tolerance);
3017 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
3018 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
3019 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
3020 }
3021 Ok(())
3022 }
3023}