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