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