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::unwrap_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].unwrap_to_mm(),
397 y: offset[1].unwrap_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].unwrap_to_mm(),
481 y: offset[1].unwrap_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::NormalizedPos {
1059 pos: KPoint2d { x: 0.0, y: 0.0 },
1060 },
1061 },
1062 DistanceEndpoint {
1063 entity_id,
1064 edge_reference,
1065 entity_pos: AnnotationMbdLeaderPosition::NormalizedPos {
1066 pos: KPoint2d { x: 1.0, y: 0.0 },
1067 },
1068 },
1069 &tolerance,
1070 precision,
1071 frame_position.as_ref(),
1072 frame_plane.id,
1073 leader_scale.as_ref(),
1074 font_size.as_ref(),
1075 exec_state,
1076 args,
1077 &mut annotations,
1078 )
1079 .await?;
1080 }
1081 Ok(annotations)
1082}
1083
1084#[allow(clippy::too_many_arguments)]
1085async fn create_basic_distance_annotation(
1086 from: DistanceEndpoint,
1087 to: DistanceEndpoint,
1088 tolerance: &Option<TyF64>,
1089 precision: u32,
1090 frame_position: Option<&[TyF64; 2]>,
1091 frame_plane_id: uuid::Uuid,
1092 leader_scale: Option<&TyF64>,
1093 font_size: Option<&TyF64>,
1094 exec_state: &mut ExecState,
1095 args: &Args,
1096 annotations: &mut Vec<GdtAnnotation>,
1097) -> Result<(), KclError> {
1098 let meta = vec![Metadata::from(args.source_range)];
1099 let annotation_id = exec_state.next_uuid();
1100 let display_units = exec_state.length_unit();
1101 let dimension = AnnotationBasicDimension::builder()
1102 .maybe_from_entity_id(from.entity_id)
1103 .maybe_from_edge_reference(from.edge_reference)
1104 .from_entity_leader_pos(from.entity_pos)
1105 .maybe_to_entity_id(to.entity_id)
1106 .maybe_to_edge_reference(to.edge_reference)
1107 .to_entity_leader_pos(to.entity_pos)
1108 .dimension(
1109 AnnotationMbdBasicDimension::builder()
1110 .tolerance(
1111 tolerance
1112 .as_ref()
1113 .map(|tol| tol.unwrap_to_length_units(display_units))
1114 .unwrap_or_default(),
1115 )
1116 .build(),
1117 )
1118 .plane_id(frame_plane_id)
1119 .offset(if let Some(offset) = frame_position {
1120 KPoint2d {
1121 x: offset[0].unwrap_to_mm(),
1122 y: offset[1].unwrap_to_mm(),
1123 }
1124 } else {
1125 KPoint2d { x: 100.0, y: 100.0 }
1126 })
1127 .precision(precision)
1128 .font_scale(gdt_font_scale(font_size, args)?)
1129 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1130 .arrow_scale(gdt_dimension_leader_scale(leader_scale, args)?)
1131 .build();
1132 let annotation_name = gdt_annotation_name(exec_state, args)?;
1133 let options = AnnotationOptions::builder()
1134 .dimension(dimension)
1135 .units(display_units.to_kcmc())
1136 .maybe_name(annotation_name)
1137 .build();
1138 let annotation_cmd = ModelingCmd::from(
1139 mcmd::NewAnnotation::builder()
1140 .options(options)
1141 .clobber(false)
1142 .annotation_type(AnnotationType::T3D)
1143 .build(),
1144 );
1145 let cmd_meta = ModelingCmdMeta::from_args_id(exec_state, args, annotation_id);
1146 exec_state.batch_modeling_cmd(cmd_meta, annotation_cmd).await?;
1147 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1148 annotations.push(GdtAnnotation {
1149 id: annotation_id,
1150 meta,
1151 });
1152 Ok(())
1153}
1154
1155pub async fn angularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1156 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1157 "faces",
1158 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1159 exec_state,
1160 )?;
1161 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1162 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1163 "datums",
1164 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1165 exec_state,
1166 )?;
1167 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1168 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1169 let frame_position: Option<[TyF64; 2]> =
1170 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1171 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1172 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1173 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1174
1175 let annotations = create_feature_control_annotations(
1176 GdtFeatureControlKind::Angularity,
1177 GdtFeatureControlParams {
1178 faces: faces.unwrap_or_default(),
1179 edges,
1180 datums,
1181 tolerance,
1182 precision,
1183 frame_position,
1184 frame_plane,
1185 leader_scale,
1186 font_size,
1187 },
1188 exec_state,
1189 &args,
1190 )
1191 .await?;
1192 Ok(annotations.into())
1193}
1194
1195pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1196 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1197 "faces",
1198 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1199 exec_state,
1200 )?;
1201 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1202 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1203 "datums",
1204 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1205 exec_state,
1206 )?;
1207 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1208 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1209 let frame_position: Option<[TyF64; 2]> =
1210 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1211 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1212 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1213 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1214
1215 let annotations = create_feature_control_annotations(
1216 GdtFeatureControlKind::Perpendicularity,
1217 GdtFeatureControlParams {
1218 faces: faces.unwrap_or_default(),
1219 edges,
1220 datums,
1221 tolerance,
1222 precision,
1223 frame_position,
1224 frame_plane,
1225 leader_scale,
1226 font_size,
1227 },
1228 exec_state,
1229 &args,
1230 )
1231 .await?;
1232 Ok(annotations.into())
1233}
1234
1235pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1236 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1237 "faces",
1238 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1239 exec_state,
1240 )?;
1241 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1242 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1243 "datums",
1244 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1245 exec_state,
1246 )?;
1247 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1248 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1249 let frame_position: Option<[TyF64; 2]> =
1250 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1251 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1252 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1253 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1254
1255 let annotations = create_feature_control_annotations(
1256 GdtFeatureControlKind::Parallelism,
1257 GdtFeatureControlParams {
1258 faces: faces.unwrap_or_default(),
1259 edges,
1260 datums,
1261 tolerance,
1262 precision,
1263 frame_position,
1264 frame_plane,
1265 leader_scale,
1266 font_size,
1267 },
1268 exec_state,
1269 &args,
1270 )
1271 .await?;
1272 Ok(annotations.into())
1273}
1274
1275pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1276 let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
1277 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1278 "faces",
1279 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1280 exec_state,
1281 )?;
1282 let edges = parse_gdt_edges_arg(exec_state, &args).await?;
1283 let frame_position: Option<[TyF64; 2]> =
1284 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1285 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1286 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1287 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1288
1289 let annotations = inner_annotation(
1290 annotation,
1291 faces.unwrap_or_default(),
1292 edges,
1293 frame_position,
1294 frame_plane,
1295 leader_scale,
1296 font_size,
1297 exec_state,
1298 &args,
1299 )
1300 .await?;
1301 Ok(annotations.into())
1302}
1303
1304#[allow(clippy::too_many_arguments)]
1305async fn inner_annotation(
1306 annotation: String,
1307 faces: Vec<TagIdentifier>,
1308 edges: Vec<GdtEdgeReference>,
1309 frame_position: Option<[TyF64; 2]>,
1310 frame_plane: Option<Plane>,
1311 leader_scale: Option<TyF64>,
1312 font_size: Option<TyF64>,
1313 exec_state: &mut ExecState,
1314 args: &Args,
1315) -> Result<Vec<GdtAnnotation>, KclError> {
1316 if annotation.is_empty() {
1317 return Err(KclError::new_semantic(KclErrorDetails::new(
1318 "Annotation text must not be empty.".to_owned(),
1319 vec![args.source_range],
1320 )));
1321 }
1322 if faces.is_empty() && edges.is_empty() {
1323 return Err(KclError::new_semantic(KclErrorDetails::new(
1324 "Annotation requires at least one face or edge.".to_owned(),
1325 vec![args.source_range],
1326 )));
1327 }
1328
1329 let mut frame_plane = if let Some(plane) = frame_plane {
1330 plane
1331 } else {
1332 xy_plane(exec_state, args).await?
1333 };
1334 ensure_sketch_plane_in_engine(
1335 &mut frame_plane,
1336 exec_state,
1337 &args.ctx,
1338 args.source_range,
1339 args.node_path.clone(),
1340 )
1341 .await?;
1342
1343 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1344 for face in &faces {
1345 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1346 create_annotation(
1347 Some(face_id),
1348 None,
1349 &annotation,
1350 frame_position.as_ref(),
1351 frame_plane.id,
1352 leader_scale.as_ref(),
1353 font_size.as_ref(),
1354 exec_state,
1355 args,
1356 &mut annotations,
1357 )
1358 .await?;
1359 }
1360 for edge in &edges {
1361 match edge {
1362 GdtEdgeReference::Entity(edge) => {
1363 create_annotation(
1364 Some(edge.get_engine_id(exec_state, args)?),
1365 None,
1366 &annotation,
1367 frame_position.as_ref(),
1368 frame_plane.id,
1369 leader_scale.as_ref(),
1370 font_size.as_ref(),
1371 exec_state,
1372 args,
1373 &mut annotations,
1374 )
1375 .await?;
1376 }
1377 GdtEdgeReference::Specifier(edge_reference) => {
1378 create_annotation(
1379 None,
1380 Some(edge_reference.clone()),
1381 &annotation,
1382 frame_position.as_ref(),
1383 frame_plane.id,
1384 leader_scale.as_ref(),
1385 font_size.as_ref(),
1386 exec_state,
1387 args,
1388 &mut annotations,
1389 )
1390 .await?;
1391 }
1392 }
1393 }
1394
1395 Ok(annotations)
1396}
1397
1398fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
1399 if let Some(precision) = precision {
1400 let rounded = precision.n.round();
1401 if !(0.0..=9.0).contains(&rounded) {
1402 return Err(KclError::new_semantic(KclErrorDetails::new(
1403 "Precision must be between 0 and 9".to_owned(),
1404 vec![args.source_range],
1405 )));
1406 }
1407 Ok(rounded as u32)
1408 } else {
1409 Ok(3)
1410 }
1411}
1412
1413async fn resolve_gdt_frame_plane(
1414 frame_plane: Option<Plane>,
1415 exec_state: &mut ExecState,
1416 args: &Args,
1417) -> Result<Plane, KclError> {
1418 let mut frame_plane = if let Some(plane) = frame_plane {
1419 plane
1420 } else {
1421 xy_plane(exec_state, args).await?
1423 };
1424 ensure_sketch_plane_in_engine(
1425 &mut frame_plane,
1426 exec_state,
1427 &args.ctx,
1428 args.source_range,
1429 args.node_path.clone(),
1430 )
1431 .await?;
1432 Ok(frame_plane)
1433}
1434
1435async fn create_feature_control_annotations(
1436 kind: GdtFeatureControlKind,
1437 params: GdtFeatureControlParams,
1438 exec_state: &mut ExecState,
1439 args: &Args,
1440) -> Result<Vec<GdtAnnotation>, KclError> {
1441 let GdtFeatureControlParams {
1442 faces,
1443 edges,
1444 datums,
1445 tolerance,
1446 precision,
1447 frame_position,
1448 frame_plane,
1449 leader_scale,
1450 font_size,
1451 } = params;
1452
1453 if faces.is_empty() && edges.is_empty() {
1454 return Err(KclError::new_semantic(KclErrorDetails::new(
1455 format!("{} requires at least one face or edge.", kind.label()),
1456 vec![args.source_range],
1457 )));
1458 }
1459
1460 let precision = resolve_precision(precision, args)?;
1461 let datums = resolve_datums(datums, args, kind.label())?;
1462 if kind.requires_datums() && datums.is_empty() {
1463 return Err(KclError::new_semantic(KclErrorDetails::new(
1464 format!("{} requires at least one datum.", kind.label()),
1465 vec![args.source_range],
1466 )));
1467 }
1468 let frame_plane = resolve_gdt_frame_plane(frame_plane, exec_state, args).await?;
1469 let symbol = kind.symbol();
1470 let diameter_symbol = kind.diameter_symbol();
1471
1472 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1473 for face in &faces {
1474 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1475 create_feature_control_annotation(
1476 Some(face_id),
1477 None,
1478 symbol,
1479 diameter_symbol,
1480 &tolerance,
1481 &datums,
1482 precision,
1483 frame_position.as_ref(),
1484 frame_plane.id,
1485 leader_scale.as_ref(),
1486 font_size.as_ref(),
1487 exec_state,
1488 args,
1489 &mut annotations,
1490 )
1491 .await?;
1492 }
1493 for edge in &edges {
1494 match edge {
1495 GdtEdgeReference::Entity(edge) => {
1496 create_feature_control_annotation(
1497 Some(edge.get_engine_id(exec_state, args)?),
1498 None,
1499 symbol,
1500 diameter_symbol,
1501 &tolerance,
1502 &datums,
1503 precision,
1504 frame_position.as_ref(),
1505 frame_plane.id,
1506 leader_scale.as_ref(),
1507 font_size.as_ref(),
1508 exec_state,
1509 args,
1510 &mut annotations,
1511 )
1512 .await?;
1513 }
1514 GdtEdgeReference::Specifier(edge_reference) => {
1515 create_feature_control_annotation(
1516 None,
1517 Some(edge_reference.clone()),
1518 symbol,
1519 diameter_symbol,
1520 &tolerance,
1521 &datums,
1522 precision,
1523 frame_position.as_ref(),
1524 frame_plane.id,
1525 leader_scale.as_ref(),
1526 font_size.as_ref(),
1527 exec_state,
1528 args,
1529 &mut annotations,
1530 )
1531 .await?;
1532 }
1533 }
1534 }
1535
1536 Ok(annotations)
1537}
1538
1539#[allow(clippy::too_many_arguments)]
1540async fn create_feature_control_annotation(
1541 entity_id: Option<uuid::Uuid>,
1542 edge_reference: Option<kcmc::shared::EdgeSpecifier>,
1543 symbol: MbdSymbol,
1544 diameter_symbol: Option<MbdSymbol>,
1545 tolerance: &TyF64,
1546 datums: &[char],
1547 precision: u32,
1548 frame_position: Option<&[TyF64; 2]>,
1549 frame_plane_id: uuid::Uuid,
1550 leader_scale: Option<&TyF64>,
1551 font_size: Option<&TyF64>,
1552 exec_state: &mut ExecState,
1553 args: &Args,
1554 annotations: &mut Vec<GdtAnnotation>,
1555) -> Result<(), KclError> {
1556 let meta = vec![Metadata::from(args.source_range)];
1557 let annotation_id = exec_state.next_uuid();
1558 let display_units = exec_state.length_unit();
1559 let control_frame = gdt_control_frame(
1560 symbol,
1561 diameter_symbol,
1562 tolerance.unwrap_to_length_units(display_units),
1563 datums,
1564 );
1565 let feature_control = AnnotationFeatureControl::builder()
1566 .maybe_entity_id(entity_id)
1567 .maybe_edge_reference(edge_reference)
1568 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1569 .leader_type(AnnotationLineEnd::Dot)
1570 .control_frame(control_frame)
1571 .plane_id(frame_plane_id)
1572 .offset(if let Some(offset) = frame_position {
1573 KPoint2d {
1574 x: offset[0].unwrap_to_mm(),
1575 y: offset[1].unwrap_to_mm(),
1576 }
1577 } else {
1578 KPoint2d { x: 100.0, y: 100.0 }
1579 })
1580 .precision(precision)
1581 .font_scale(gdt_font_scale(font_size, args)?)
1582 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1583 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1584 .build();
1585 let annotation_name = gdt_annotation_name(exec_state, args)?;
1586 let options = AnnotationOptions::builder()
1587 .feature_control(feature_control)
1588 .maybe_name(annotation_name)
1589 .build();
1590 exec_state
1591 .batch_modeling_cmd(
1592 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1593 ModelingCmd::from(
1594 mcmd::NewAnnotation::builder()
1595 .options(options)
1596 .clobber(false)
1597 .annotation_type(AnnotationType::T3D)
1598 .build(),
1599 ),
1600 )
1601 .await?;
1602 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1603 annotations.push(GdtAnnotation {
1604 id: annotation_id,
1605 meta,
1606 });
1607 Ok(())
1608}
1609
1610fn gdt_control_frame(
1611 symbol: MbdSymbol,
1612 diameter_symbol: Option<MbdSymbol>,
1613 tolerance: f64,
1614 datums: &[char],
1615) -> AnnotationMbdControlFrame {
1616 match datums {
1617 [] => AnnotationMbdControlFrame::builder()
1618 .symbol(symbol)
1619 .maybe_diameter_symbol(diameter_symbol)
1620 .tolerance(tolerance)
1621 .build(),
1622 [primary] => AnnotationMbdControlFrame::builder()
1623 .symbol(symbol)
1624 .maybe_diameter_symbol(diameter_symbol)
1625 .tolerance(tolerance)
1626 .primary_datum(*primary)
1627 .build(),
1628 [primary, secondary] => AnnotationMbdControlFrame::builder()
1629 .symbol(symbol)
1630 .maybe_diameter_symbol(diameter_symbol)
1631 .tolerance(tolerance)
1632 .primary_datum(*primary)
1633 .secondary_datum(*secondary)
1634 .build(),
1635 [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1636 .symbol(symbol)
1637 .maybe_diameter_symbol(diameter_symbol)
1638 .tolerance(tolerance)
1639 .primary_datum(*primary)
1640 .secondary_datum(*secondary)
1641 .tertiary_datum(*tertiary)
1642 .build(),
1643 _ => unreachable!("resolve_datums rejects more than three datums"),
1644 }
1645}
1646
1647#[allow(clippy::too_many_arguments)]
1648async fn create_annotation(
1649 entity_id: Option<uuid::Uuid>,
1650 edge_reference: Option<kcmc::shared::EdgeSpecifier>,
1651 annotation: &str,
1652 frame_position: Option<&[TyF64; 2]>,
1653 frame_plane_id: uuid::Uuid,
1654 leader_scale: Option<&TyF64>,
1655 font_size: Option<&TyF64>,
1656 exec_state: &mut ExecState,
1657 args: &Args,
1658 annotations: &mut Vec<GdtAnnotation>,
1659) -> Result<(), KclError> {
1660 let meta = vec![Metadata::from(args.source_range)];
1661 let annotation_id = exec_state.next_uuid();
1662 let feature_control = AnnotationFeatureControl::builder()
1663 .maybe_entity_id(entity_id)
1664 .maybe_edge_reference(edge_reference)
1665 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1666 .leader_type(AnnotationLineEnd::Dot)
1667 .prefix(annotation.to_owned())
1668 .plane_id(frame_plane_id)
1669 .offset(if let Some(offset) = frame_position {
1670 KPoint2d {
1671 x: offset[0].unwrap_to_mm(),
1672 y: offset[1].unwrap_to_mm(),
1673 }
1674 } else {
1675 KPoint2d { x: 100.0, y: 100.0 }
1676 })
1677 .precision(0)
1678 .font_scale(gdt_font_scale(font_size, args)?)
1679 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1680 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1681 .build();
1682 let annotation_name = gdt_annotation_name(exec_state, args)?;
1683 let options = AnnotationOptions::builder()
1684 .feature_control(feature_control)
1685 .maybe_name(annotation_name)
1686 .build();
1687 exec_state
1688 .batch_modeling_cmd(
1689 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1690 ModelingCmd::from(
1691 mcmd::NewAnnotation::builder()
1692 .options(options)
1693 .clobber(false)
1694 .annotation_type(AnnotationType::T3D)
1695 .build(),
1696 ),
1697 )
1698 .await?;
1699 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1700 annotations.push(GdtAnnotation {
1701 id: annotation_id,
1702 meta,
1703 });
1704 Ok(())
1705}
1706
1707fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1708 let datums = datums.unwrap_or_default();
1709 if datums.len() > 3 {
1710 return Err(KclError::new_semantic(KclErrorDetails::new(
1711 format!("{annotation_name} datums must include at most three names."),
1712 vec![args.source_range],
1713 )));
1714 }
1715
1716 let mut resolved = Vec::with_capacity(datums.len());
1717 for datum in &datums {
1718 let mut chars = datum.chars();
1719 let Some(name) = chars.next() else {
1720 return Err(KclError::new_semantic(KclErrorDetails::new(
1721 format!("{annotation_name} datum names must be a single character."),
1722 vec![args.source_range],
1723 )));
1724 };
1725 if chars.next().is_some() {
1726 return Err(KclError::new_semantic(KclErrorDetails::new(
1727 format!("{annotation_name} datum names must be a single character."),
1728 vec![args.source_range],
1729 )));
1730 }
1731 resolved.push(name);
1732 }
1733
1734 Ok(resolved)
1735}
1736
1737async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1740 let plane_ast = plane_ast("XY", args.source_range);
1741 let metadata = Metadata::from(args.source_range);
1742 let plane_value = args.ctx.eval_expr_fresh_root(&plane_ast, exec_state, &metadata).await?;
1743 let plane_value = plane_value.into_value();
1745 Ok(plane_value
1746 .as_plane()
1747 .ok_or_else(|| {
1748 KclError::new_internal(KclErrorDetails::new(
1749 "Expected XY plane to be defined".to_owned(),
1750 vec![args.source_range],
1751 ))
1752 })?
1753 .clone())
1754}
1755
1756fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1758 ast::Node::new(
1759 ast::Expr::Name(BoxNode::new(ast::Node::new(
1760 ast::Name {
1761 name: ast::Identifier::new(plane_name),
1762 path: Vec::new(),
1763 abs_path: false,
1766 digest: None,
1767 },
1768 range.start(),
1769 range.end(),
1770 range.module_id(),
1771 ))),
1772 range.start(),
1773 range.end(),
1774 range.module_id(),
1775 )
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780 use super::*;
1781 use crate::ExecutorContext;
1782 use crate::execution::Artifact;
1783 use crate::execution::ExecutorSettings;
1784 use crate::execution::MockConfig;
1785 use crate::execution::parse_execute;
1786
1787 const GDT_DISTANCE_KCL_TEMPLATE: &str = r#"
1788@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1789
1790sketch001 = sketch(on = XY) {
1791 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1792 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1793 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1794 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1795 coincident([line1.end, line2.start])
1796 coincident([line2.end, line3.start])
1797 coincident([line3.end, line4.start])
1798 coincident([line4.end, line1.start])
1799 parallel([line2, line4])
1800 parallel([line3, line1])
1801 perpendicular([line1, line2])
1802 horizontal(line3)
1803}
1804
1805region001 = region(point = [5mm, 5mm], sketch = sketch001)
1806extrude001 = extrude(region001, length = 10mm)
1807gdt::distance(
1808 edges = [
1809 {
1810 sideFaces = [
1811 region001.tags.line4,
1812 region001.tags.line1
1813 ]
1814 }
1815 ],
1816 tolerance = __TOLERANCE__,
1817 framePosition = __FRAME_POSITION__,
1818 fontSize = 2in,
1819)
1820"#;
1821
1822 const GDT_FLATNESS_KCL_TEMPLATE: &str = r#"
1823@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1824
1825sketch001 = sketch(on = XY) {
1826 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1827 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1828 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1829 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1830 coincident([line1.end, line2.start])
1831 coincident([line2.end, line3.start])
1832 coincident([line3.end, line4.start])
1833 coincident([line4.end, line1.start])
1834 parallel([line2, line4])
1835 parallel([line3, line1])
1836 perpendicular([line1, line2])
1837 horizontal(line3)
1838}
1839
1840region001 = region(point = [5mm, 5mm], sketch = sketch001)
1841extrude001 = extrude(region001, length = 10mm, tagEnd = $capEnd001)
1842gdt::flatness(
1843 faces = [capEnd001],
1844 tolerance = __TOLERANCE__,
1845 framePosition = __FRAME_POSITION__,
1846 framePlane = XZ,
1847 fontSize = 2in,
1848)
1849"#;
1850
1851 fn gdt_distance_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1852 GDT_DISTANCE_KCL_TEMPLATE
1853 .replace("__UNIT__", unit)
1854 .replace("__TOLERANCE__", tolerance)
1855 .replace("__FRAME_POSITION__", frame_position)
1856 }
1857
1858 fn gdt_flatness_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1859 GDT_FLATNESS_KCL_TEMPLATE
1860 .replace("__UNIT__", unit)
1861 .replace("__TOLERANCE__", tolerance)
1862 .replace("__FRAME_POSITION__", frame_position)
1863 }
1864
1865 async fn gdt_commands(code: &str) -> Vec<ModelingCmd> {
1866 let result = parse_execute(code).await.unwrap();
1867 result
1868 .root_module_artifact_commands()
1869 .iter()
1870 .map(|artifact_command| artifact_command.command.clone())
1871 .collect()
1872 }
1873
1874 fn annotation_options(command: &ModelingCmd) -> Result<&AnnotationOptions, KclError> {
1875 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1876 return Err(KclError::new_internal(KclErrorDetails::new(
1877 format!("expected new_annotation command, got {command:?}"),
1878 vec![SourceRange::default()],
1879 )));
1880 };
1881 Ok(&new_annotation.options)
1882 }
1883
1884 fn feature_control(command: &ModelingCmd) -> Result<&AnnotationFeatureControl, KclError> {
1885 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1886 return Err(KclError::new_internal(KclErrorDetails::new(
1887 format!("expected new_annotation command, got {command:?}"),
1888 vec![SourceRange::default()],
1889 )));
1890 };
1891 new_annotation.options.feature_control.as_ref().ok_or_else(|| {
1892 KclError::new_internal(KclErrorDetails::new(
1893 "expected new_annotation command to have a feature_control".to_owned(),
1894 vec![SourceRange::default()],
1895 ))
1896 })
1897 }
1898
1899 fn find_control_frame_with_symbol(
1900 commands: &[ModelingCmd],
1901 symbol: MbdSymbol,
1902 ) -> Result<&AnnotationMbdControlFrame, KclError> {
1903 for command in commands {
1904 if let Ok(feature_control) = feature_control(command)
1905 && let Some(control_frame) = feature_control.control_frame.as_ref()
1906 && control_frame.symbol == symbol
1907 {
1908 return Ok(control_frame);
1909 }
1910 }
1911
1912 Err(KclError::new_internal(KclErrorDetails::new(
1913 format!("expected commands to contain a {symbol:?} control frame"),
1914 vec![SourceRange::default()],
1915 )))
1916 }
1917
1918 #[track_caller]
1919 fn assert_close(actual: f64, expected: f64) {
1920 assert!((actual - expected).abs() < 1e-6, "expected {expected}, got {actual}");
1921 }
1922
1923 fn new_annotation_command_index(commands: &[ModelingCmd]) -> Result<usize, KclError> {
1924 commands
1925 .iter()
1926 .position(|command| matches!(command, ModelingCmd::NewAnnotation(_)))
1927 .ok_or_else(|| {
1928 KclError::new_internal(KclErrorDetails::new(
1929 "expected commands to contain a new_annotation command".to_owned(),
1930 vec![SourceRange::default()],
1931 ))
1932 })
1933 }
1934
1935 #[tokio::test(flavor = "multi_thread")]
1936 async fn gdt_annotation_name_comes_from_explicit_argument() -> Result<(), KclError> {
1937 let unbound_code = gdt_flatness_kcl("mm", "0.01mm", "[10, -10]");
1938 let unbound_commands = gdt_commands(&unbound_code).await;
1939 let unbound_index = new_annotation_command_index(&unbound_commands)?;
1940 assert_eq!(annotation_options(&unbound_commands[unbound_index])?.name, None);
1941
1942 let assigned_code = unbound_code.replacen("gdt::flatness(", "topFlatness = gdt::flatness(", 1);
1943 let assigned_commands = gdt_commands(&assigned_code).await;
1944 let assigned_index = new_annotation_command_index(&assigned_commands)?;
1945 assert_eq!(annotation_options(&assigned_commands[assigned_index])?.name, None);
1946
1947 let named_code = unbound_code.replacen(
1948 "gdt::flatness(\n",
1949 "gdt::flatness(\n annotationName = \"topFlatness\",\n",
1950 1,
1951 );
1952 let named_commands = gdt_commands(&named_code).await;
1953 let named_index = new_annotation_command_index(&named_commands)?;
1954 assert_eq!(
1955 annotation_options(&named_commands[named_index])?.name.as_deref(),
1956 Some("topFlatness")
1957 );
1958
1959 Ok(())
1960 }
1961
1962 #[test]
1963 fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1964 let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1965 assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1966
1967 let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1968 assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1969
1970 let inch_in_mm = 25.4;
1971 let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1972 assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1973 }
1974
1975 const GDT_FLATNESS_LEADER_KCL_TEMPLATE: &str = r#"
1976@settings(defaultLengthUnit = mm, kclVersion = 2)
1977
1978blockProfile = sketch(on = XY) {
1979 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1980 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1981 edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1982 edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1983 coincident([edge1.end, edge2.start])
1984 coincident([edge2.end, edge3.start])
1985 coincident([edge3.end, edge4.start])
1986 coincident([edge4.end, edge1.start])
1987 parallel([edge2, edge4])
1988 parallel([edge3, edge1])
1989 perpendicular([edge1, edge2])
1990 horizontal(edge3)
1991}
1992
1993region001 = region(point = [5mm, 5mm], sketch = blockProfile)
1994extrude001 = extrude(region001, length = 10mm, tagEnd = $top)
1995gdt::flatness(
1996 faces = [top],
1997 tolerance = 0.1mm,
1998 framePosition = [10mm, 0mm],
1999 framePlane = XZ,
2000 fontSize = __FONT_SIZE__
2001 __LEADER_SCALE__
2002)
2003"#;
2004
2005 fn gdt_flatness_leader_kcl(font_size: &str, leader_scale: Option<&str>) -> String {
2006 GDT_FLATNESS_LEADER_KCL_TEMPLATE
2007 .replace("__FONT_SIZE__", font_size)
2008 .replace(
2009 "__LEADER_SCALE__",
2010 leader_scale
2011 .map(|scale| format!(",\n leaderScale = {scale}"))
2012 .unwrap_or_default()
2013 .as_str(),
2014 )
2015 }
2016
2017 async fn gdt_flatness_feature_control(
2018 font_size: &str,
2019 leader_scale: Option<&str>,
2020 ) -> Result<AnnotationFeatureControl, KclError> {
2021 let code = gdt_flatness_leader_kcl(font_size, leader_scale);
2022 let commands = gdt_commands(&code).await;
2023 let annotation_index = new_annotation_command_index(&commands)?;
2024 Ok(feature_control(&commands[annotation_index])?.clone())
2025 }
2026
2027 #[tokio::test(flavor = "multi_thread")]
2028 async fn gdt_dot_leader_scale_is_normalized_against_font_scale() -> Result<(), KclError> {
2029 let tiny = gdt_flatness_feature_control("1mm", None).await?;
2030 let large = gdt_flatness_feature_control("100mm", None).await?;
2031
2032 assert_close(f64::from(tiny.font_scale), gdt_font_scale_for_height_mm(1.0).into());
2033 assert_close(f64::from(large.font_scale), gdt_font_scale_for_height_mm(100.0).into());
2034 assert_close(f64::from(tiny.leader_scale), 50.0);
2035 assert_close(f64::from(large.leader_scale), 0.5);
2036
2037 assert_close(
2038 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
2039 f64::from(gdt_dot_leader_normal_size()),
2040 );
2041 assert_close(
2042 f64::from(large.font_scale) * f64::from(large.leader_scale),
2043 f64::from(gdt_dot_leader_normal_size()),
2044 );
2045 Ok(())
2046 }
2047
2048 #[tokio::test(flavor = "multi_thread")]
2049 async fn explicit_gdt_dot_leader_scale_multiplies_normal_size() -> Result<(), KclError> {
2050 let tiny = gdt_flatness_feature_control("1mm", Some("2")).await?;
2051 let large = gdt_flatness_feature_control("100mm", Some("2")).await?;
2052
2053 let expected_scaled_dot_size = f64::from(gdt_dot_leader_normal_size()) * 2.0;
2054 assert_close(
2055 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
2056 expected_scaled_dot_size,
2057 );
2058 assert_close(
2059 f64::from(large.font_scale) * f64::from(large.leader_scale),
2060 expected_scaled_dot_size,
2061 );
2062 Ok(())
2063 }
2064
2065 #[tokio::test(flavor = "multi_thread")]
2066 async fn gdt_flatness_uses_scene_units_for_control_frame_tolerance() -> Result<(), KclError> {
2067 let cases = [
2068 ("in", "0.1in", "[10, -10]", 0.1, 254.0, -254.0),
2069 ("cm", "10mm", "[1, -1]", 1.0, 10.0, -10.0),
2070 ];
2071
2072 for (default_unit, tolerance, frame_position, expected_tolerance, expected_x, expected_y) in cases {
2073 let code = gdt_flatness_kcl(default_unit, tolerance, frame_position);
2074 let commands = gdt_commands(&code).await;
2075 let annotation_index = new_annotation_command_index(&commands)?;
2076 let feature_control = feature_control(&commands[annotation_index])?;
2077 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
2078 KclError::new_internal(KclErrorDetails::new(
2079 "expected feature_control to have a control_frame".to_owned(),
2080 vec![SourceRange::default()],
2081 ))
2082 })?;
2083
2084 assert_close(control_frame.tolerance, expected_tolerance);
2085 assert_close(feature_control.offset.x, expected_x);
2086 assert_close(feature_control.offset.y, expected_y);
2087 assert_close(
2088 f64::from(feature_control.font_scale),
2089 gdt_font_scale_for_height_mm(50.8).into(),
2090 );
2091 }
2092 Ok(())
2093 }
2094
2095 #[tokio::test(flavor = "multi_thread")]
2096 async fn gdt_distance_sets_units() -> Result<(), KclError> {
2097 let cases = [
2098 (
2099 "in",
2100 "2.54mm",
2101 "[10, -10]",
2102 kcmc::units::UnitLength::Inches,
2103 0.1,
2104 254.0,
2105 -254.0,
2106 ),
2107 (
2108 "cm",
2109 "10mm",
2110 "[1, -1]",
2111 kcmc::units::UnitLength::Centimeters,
2112 1.0,
2113 10.0,
2114 -10.0,
2115 ),
2116 (
2117 "mm",
2118 "2.54mm",
2119 "[10, -10]",
2120 kcmc::units::UnitLength::Millimeters,
2121 2.54,
2122 10.0,
2123 -10.0,
2124 ),
2125 ];
2126
2127 for (default_unit, tolerance, frame_position, scene_unit, expected_tolerance, expected_x, expected_y) in cases {
2128 let code = gdt_distance_kcl(default_unit, tolerance, frame_position);
2129 let commands = gdt_commands(&code).await;
2130 let annotation_index = new_annotation_command_index(&commands)?;
2131 let options = annotation_options(&commands[annotation_index])?;
2132
2133 assert_eq!(options.units, Some(scene_unit));
2134
2135 let dimension = options
2136 .dimension
2137 .as_ref()
2138 .expect("expected new_annotation command to have a dimension");
2139 assert!(dimension.from_entity_id.is_none());
2142 assert!(dimension.to_entity_id.is_none());
2143 assert_eq!(dimension.from_edge_reference, dimension.to_edge_reference);
2144 assert_eq!(
2145 dimension
2146 .from_edge_reference
2147 .as_ref()
2148 .expect("expected from_edge_reference")
2149 .side_faces
2150 .len(),
2151 2
2152 );
2153 assert_eq!(
2155 dimension.from_entity_leader_pos,
2156 Some(AnnotationMbdLeaderPosition::NormalizedPos {
2157 pos: KPoint2d { x: 0.0, y: 0.0 },
2158 })
2159 );
2160 assert_eq!(
2161 dimension.to_entity_leader_pos,
2162 Some(AnnotationMbdLeaderPosition::NormalizedPos {
2163 pos: KPoint2d { x: 1.0, y: 0.0 },
2164 })
2165 );
2166 assert_close(dimension.dimension.tolerance.unwrap(), expected_tolerance);
2167 assert_close(dimension.offset.x, expected_x);
2168 assert_close(dimension.offset.y, expected_y);
2169 assert_close(
2170 f64::from(dimension.font_scale),
2171 gdt_font_scale_for_height_mm(50.8).into(),
2172 );
2173 }
2174 Ok(())
2175 }
2176
2177 const GDT_FACE_API_EDGE_KCL_TEMPLATE: &str = r#"
2178@settings(defaultLengthUnit = mm, kclVersion = 2)
2179
2180sketch001 = sketch(on = XY) {
2181 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2182 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
2183 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
2184 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
2185 coincident([line1.end, line2.start])
2186 coincident([line2.end, line3.start])
2187 coincident([line3.end, line4.start])
2188 coincident([line4.end, line1.start])
2189 parallel([line2, line4])
2190 parallel([line3, line1])
2191 perpendicular([line1, line2])
2192 horizontal(line3)
2193}
2194
2195region001 = region(point = [5mm, 5mm], sketch = sketch001)
2196extrude001 = extrude(region001, length = 10mm, tagStart = $capStart001)
2197__GDT_CALL__
2198"#;
2199
2200 fn gdt_face_api_edge_kcl(gdt_call: &str) -> String {
2201 GDT_FACE_API_EDGE_KCL_TEMPLATE.replace("__GDT_CALL__", gdt_call)
2202 }
2203
2204 fn assert_feature_control_uses_edge_reference(feature_control: &AnnotationFeatureControl) {
2205 assert!(feature_control.entity_id.is_none());
2206 let edge_reference = feature_control
2207 .edge_reference
2208 .as_ref()
2209 .expect("expected face API edge specifier to emit edge_reference");
2210 assert_eq!(edge_reference.side_faces.len(), 2);
2211 assert!(edge_reference.end_faces.is_empty());
2212 assert_eq!(edge_reference.index, None);
2213 }
2214
2215 #[tokio::test(flavor = "multi_thread")]
2216 async fn gdt_straightness_accepts_face_api_edge_specifier() -> Result<(), KclError> {
2217 let code = gdt_face_api_edge_kcl(
2218 r#"gdt::straightness(
2219 edges = [
2220 {
2221 sideFaces = [region001.tags.line1, capStart001]
2222 }
2223 ],
2224 tolerance = 0.1mm,
2225 framePosition = [12mm, 8mm],
2226 framePlane = XZ,
2227)"#,
2228 );
2229 let commands = gdt_commands(&code).await;
2230 let annotation_index = new_annotation_command_index(&commands)?;
2231 let feature_control = feature_control(&commands[annotation_index])?;
2232 assert_feature_control_uses_edge_reference(feature_control);
2233 assert!(feature_control.control_frame.is_some());
2234 Ok(())
2235 }
2236
2237 #[tokio::test(flavor = "multi_thread")]
2238 async fn gdt_annotation_accepts_face_api_edge_specifier() -> Result<(), KclError> {
2239 let code = gdt_face_api_edge_kcl(
2240 r#"gdt::annotation(
2241 annotation = "A",
2242 edges = [
2243 {
2244 sideFaces = [region001.tags.line1, capStart001]
2245 }
2246 ],
2247 framePosition = [12mm, 8mm],
2248 framePlane = XZ,
2249)"#,
2250 );
2251 let commands = gdt_commands(&code).await;
2252 let annotation_index = new_annotation_command_index(&commands)?;
2253 let feature_control = feature_control(&commands[annotation_index])?;
2254 assert_feature_control_uses_edge_reference(feature_control);
2255 assert_eq!(feature_control.prefix.as_deref(), Some("A"));
2256 Ok(())
2257 }
2258
2259 #[tokio::test(flavor = "multi_thread")]
2260 async fn gdt_distance_accepts_face_api_edge_specifier() -> Result<(), KclError> {
2261 let code = gdt_face_api_edge_kcl(
2262 r#"gdt::distance(
2263 edges = [
2264 {
2265 sideFaces = [region001.tags.line1, capStart001]
2266 }
2267 ],
2268 tolerance = 0.1mm,
2269 framePosition = [12mm, 8mm],
2270 framePlane = XZ,
2271)"#,
2272 );
2273 let commands = gdt_commands(&code).await;
2274 let annotation_index = new_annotation_command_index(&commands)?;
2275 let options = annotation_options(&commands[annotation_index])?;
2276 let dimension = options
2277 .dimension
2278 .as_ref()
2279 .expect("expected new_annotation command to have a dimension");
2280 assert!(dimension.from_entity_id.is_none());
2281 assert!(dimension.to_entity_id.is_none());
2282 assert_eq!(dimension.from_edge_reference, dimension.to_edge_reference);
2283 assert_eq!(
2285 dimension.from_entity_leader_pos,
2286 Some(AnnotationMbdLeaderPosition::NormalizedPos {
2287 pos: KPoint2d { x: 0.0, y: 0.0 },
2288 })
2289 );
2290 assert_eq!(
2291 dimension.to_entity_leader_pos,
2292 Some(AnnotationMbdLeaderPosition::NormalizedPos {
2293 pos: KPoint2d { x: 1.0, y: 0.0 },
2294 })
2295 );
2296 assert_eq!(
2297 dimension
2298 .from_edge_reference
2299 .as_ref()
2300 .expect("expected from_edge_reference")
2301 .side_faces
2302 .len(),
2303 2
2304 );
2305 assert_eq!(
2306 dimension
2307 .to_edge_reference
2308 .as_ref()
2309 .expect("expected to_edge_reference")
2310 .side_faces
2311 .len(),
2312 2
2313 );
2314 Ok(())
2315 }
2316
2317 #[tokio::test(flavor = "multi_thread")]
2318 async fn gdt_distance_from_to_accept_face_api_edge_specifiers() -> Result<(), KclError> {
2319 let code = gdt_face_api_edge_kcl(
2320 r#"gdt::distance(
2321 from = {
2322 sideFaces = [region001.tags.line1, capStart001]
2323 },
2324 to = {
2325 sideFaces = [region001.tags.line3, capStart001]
2326 },
2327 tolerance = 0.1mm,
2328 framePosition = [12mm, 8mm],
2329 framePlane = XZ,
2330)"#,
2331 );
2332 let commands = gdt_commands(&code).await;
2333 let annotation_index = new_annotation_command_index(&commands)?;
2334 let options = annotation_options(&commands[annotation_index])?;
2335 let dimension = options
2336 .dimension
2337 .as_ref()
2338 .expect("expected new_annotation command to have a dimension");
2339 assert!(dimension.from_entity_id.is_none());
2340 assert!(dimension.to_entity_id.is_none());
2341 assert_eq!(
2343 dimension.from_entity_leader_pos,
2344 Some(AnnotationMbdLeaderPosition::Centroid {})
2345 );
2346 assert_eq!(
2347 dimension.to_entity_leader_pos,
2348 Some(AnnotationMbdLeaderPosition::Centroid {})
2349 );
2350 assert_eq!(
2351 dimension
2352 .from_edge_reference
2353 .as_ref()
2354 .expect("expected from_edge_reference")
2355 .side_faces
2356 .len(),
2357 2
2358 );
2359 assert_eq!(
2360 dimension
2361 .to_edge_reference
2362 .as_ref()
2363 .expect("expected to_edge_reference")
2364 .side_faces
2365 .len(),
2366 2
2367 );
2368 Ok(())
2369 }
2370
2371 const GDT_DATUM_KCL: &str = r#"
2372blockProfile = sketch(on = XY) {
2373 edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
2374 edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
2375 edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
2376 edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
2377 coincident([edge1.end, edge2.start])
2378 coincident([edge2.end, edge3.start])
2379 coincident([edge3.end, edge4.start])
2380 coincident([edge4.end, edge1.start])
2381 horizontal(edge1)
2382 vertical(edge2)
2383 horizontal(edge3)
2384 vertical(edge4)
2385}
2386
2387block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2388
2389gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
2390"#;
2391
2392 async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
2393 let settings = ExecutorSettings {
2394 skip_artifact_graph,
2395 ..Default::default()
2396 };
2397 let ctx = ExecutorContext::new_mock(Some(settings)).await;
2398 let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
2399 let mock_config = MockConfig {
2400 use_prev_memory: false,
2401 ..Default::default()
2402 };
2403 let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
2404 ctx.close().await;
2405
2406 outcome
2407 .artifact_graph
2408 .values()
2409 .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
2410 .count()
2411 }
2412
2413 #[tokio::test(flavor = "multi_thread")]
2414 async fn gdt_annotations_do_not_follow_runtime_artifact_graph_setting() {
2415 assert_eq!(gdt_artifact_count(false).await, 1);
2416 assert_eq!(gdt_artifact_count(true).await, 1);
2417 }
2418
2419 const GDT_ANGULARITY_FACE_KCL: &str = r#"
2420@settings(defaultLengthUnit = mm, kclVersion = 2)
2421
2422basicAngle = 30deg
2423thickness = 3.5mm
2424flangeLength = 24mm
2425bendStartX = 5mm
2426legLength = 30mm
2427legRun = legLength * cos(basicAngle)
2428legRise = legLength * sin(basicAngle)
2429normalRun = thickness * sin(basicAngle)
2430normalRise = thickness * cos(basicAngle)
2431annotationFont = 2mm
2432
2433stampedProfile = sketch(on = XY) {
2434 datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
2435 flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
2436 innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
2437 controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
2438 tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
2439 outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
2440 outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
2441 coincident([datumFace.end, flangeEnd.start])
2442 coincident([flangeEnd.end, innerFlange.start])
2443 coincident([innerFlange.end, controlledSurface.start])
2444 coincident([controlledSurface.end, tabEnd.start])
2445 coincident([tabEnd.end, outerSurface.start])
2446 coincident([outerSurface.end, outsideBend.start])
2447 coincident([outsideBend.end, datumFace.start])
2448 coincident([datumFace.start, ORIGIN])
2449 horizontal(datumFace)
2450 horizontal(innerFlange)
2451 vertical(flangeEnd)
2452 distance([datumFace.start, datumFace.end]) == flangeLength
2453 distance([flangeEnd.start, flangeEnd.end]) == thickness
2454 distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
2455 distance([controlledSurface.start, controlledSurface.end]) == legLength
2456 distance([tabEnd.start, tabEnd.end]) == thickness
2457 distance([outerSurface.start, outerSurface.end]) == legLength
2458 parallel([controlledSurface, outerSurface])
2459 perpendicular([controlledSurface, tabEnd])
2460 angle([datumFace, controlledSurface]) == basicAngle
2461}
2462
2463stampedPart = extrude(region(point = [12mm, 2mm], sketch = stampedProfile), length = 0.8mm)
2464
2465gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
2466gdt::angularity(faces = [stampedPart.sketch.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
2467"#;
2468
2469 const GDT_ANGULARITY_EDGE_KCL: &str = r#"
2470@settings(defaultLengthUnit = mm, kclVersion = 2)
2471
2472basicAngle = 30deg
2473thickness = 3.5mm
2474flangeLength = 24mm
2475bendStartX = 5mm
2476legLength = 30mm
2477legRun = legLength * cos(basicAngle)
2478legRise = legLength * sin(basicAngle)
2479normalRun = thickness * sin(basicAngle)
2480normalRise = thickness * cos(basicAngle)
2481annotationFont = 2mm
2482
2483stampedProfile = sketch(on = XY) {
2484 datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
2485 flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
2486 innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
2487 controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
2488 tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
2489 outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
2490 outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
2491 coincident([datumFace.end, flangeEnd.start])
2492 coincident([flangeEnd.end, innerFlange.start])
2493 coincident([innerFlange.end, controlledSurface.start])
2494 coincident([controlledSurface.end, tabEnd.start])
2495 coincident([tabEnd.end, outerSurface.start])
2496 coincident([outerSurface.end, outsideBend.start])
2497 coincident([outsideBend.end, datumFace.start])
2498 coincident([datumFace.start, ORIGIN])
2499 horizontal(datumFace)
2500 horizontal(innerFlange)
2501 vertical(flangeEnd)
2502 distance([datumFace.start, datumFace.end]) == flangeLength
2503 distance([flangeEnd.start, flangeEnd.end]) == thickness
2504 distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
2505 distance([controlledSurface.start, controlledSurface.end]) == legLength
2506 distance([tabEnd.start, tabEnd.end]) == thickness
2507 distance([outerSurface.start, outerSurface.end]) == legLength
2508 parallel([controlledSurface, outerSurface])
2509 perpendicular([controlledSurface, tabEnd])
2510 angle([datumFace, controlledSurface]) == basicAngle
2511}
2512
2513stampedRegion = region(point = [12mm, 2mm], sketch = stampedProfile)
2514hide(stampedProfile)
2515stampedPart = extrude(stampedRegion, length = 0.8mm)
2516
2517gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
2518gdt::angularity(edges = [stampedRegion.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
2519"#;
2520
2521 #[tokio::test(flavor = "multi_thread")]
2522 async fn gdt_angularity_uses_angularity_symbol_with_datums() -> Result<(), KclError> {
2523 let cases = [
2524 ("angled face", GDT_ANGULARITY_FACE_KCL, 0.1),
2525 ("angled edge", GDT_ANGULARITY_EDGE_KCL, 0.1),
2526 ];
2527
2528 for (label, code, expected_tolerance) in cases {
2529 let commands = gdt_commands(code).await;
2530 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Angularity)?;
2531
2532 assert_close(control_frame.tolerance, expected_tolerance);
2533 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
2534 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2535 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2536 }
2537 Ok(())
2538 }
2539
2540 const GDT_PROFILE_LINE_KCL: &str = r#"
2541@settings(defaultLengthUnit = mm, kclVersion = 2)
2542
2543blockProfile = sketch(on = XY) {
2544 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2545 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 6mm])
2546 edge3 = line(start = [var 10mm, var 6mm], end = [var 0mm, var 6mm])
2547 edge4 = line(start = [var 0mm, var 6mm], end = [var 0mm, var 0mm])
2548 coincident([edge1.end, edge2.start])
2549 coincident([edge2.end, edge3.start])
2550 coincident([edge3.end, edge4.start])
2551 coincident([edge4.end, edge1.start])
2552 horizontal(edge1)
2553 vertical(edge2)
2554 horizontal(edge3)
2555 vertical(edge4)
2556}
2557
2558block = extrude(region(point = [5mm, 3mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2559profileEdge = { sideFaces = [block.sketch.tags.edge1, top] }
2560gdt::profileLine(edges = [profileEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2561"#;
2562
2563 const GDT_PROFILE_GENERIC_LINE_KCL: &str = r#"
2564@settings(defaultLengthUnit = mm, kclVersion = 2)
2565
2566blockProfile = sketch(on = XY) {
2567 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2568 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 6mm])
2569 edge3 = line(start = [var 10mm, var 6mm], end = [var 0mm, var 6mm])
2570 edge4 = line(start = [var 0mm, var 6mm], end = [var 0mm, var 0mm])
2571 coincident([edge1.end, edge2.start])
2572 coincident([edge2.end, edge3.start])
2573 coincident([edge3.end, edge4.start])
2574 coincident([edge4.end, edge1.start])
2575 horizontal(edge1)
2576 vertical(edge2)
2577 horizontal(edge3)
2578 vertical(edge4)
2579}
2580
2581block = extrude(region(point = [5mm, 3mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2582profileEdge = { sideFaces = [block.sketch.tags.edge1, top] }
2583gdt::profile(edges = [profileEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2584"#;
2585
2586 const GDT_PROFILE_SURFACE_KCL: &str = r#"
2587@settings(defaultLengthUnit = mm, kclVersion = 2)
2588
2589cylinderSketch = sketch(on = XY) {
2590 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2591}
2592
2593cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2594gdt::profileSurface(faces = [top], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2595"#;
2596
2597 const GDT_PROFILE_GENERIC_SURFACE_KCL: &str = r#"
2598@settings(defaultLengthUnit = mm, kclVersion = 2)
2599
2600cylinderSketch = sketch(on = XY) {
2601 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2602}
2603
2604cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2605gdt::profile(faces = [top], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2606"#;
2607
2608 const GDT_PROFILE_BOTH_KCL: &str = r#"
2609@settings(defaultLengthUnit = mm, kclVersion = 2)
2610
2611blockProfile = sketch(on = XY) {
2612 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2613 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 6mm])
2614 edge3 = line(start = [var 10mm, var 6mm], end = [var 0mm, var 6mm])
2615 edge4 = line(start = [var 0mm, var 6mm], end = [var 0mm, var 0mm])
2616 coincident([edge1.end, edge2.start])
2617 coincident([edge2.end, edge3.start])
2618 coincident([edge3.end, edge4.start])
2619 coincident([edge4.end, edge1.start])
2620 horizontal(edge1)
2621 vertical(edge2)
2622 horizontal(edge3)
2623 vertical(edge4)
2624}
2625
2626block = extrude(region(point = [5mm, 3mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
2627profileEdge = { sideFaces = [block.sketch.tags.edge1, top] }
2628gdt::profile(edges = [profileEdge], faces = [top], tolerance = 0.05mm)
2629"#;
2630
2631 const GDT_PROFILE_MISSING_ENTITIES_KCL: &str = r#"
2632@settings(defaultLengthUnit = mm, kclVersion = 2)
2633
2634gdt::profile(tolerance = 0.05mm)
2635"#;
2636
2637 #[tokio::test(flavor = "multi_thread")]
2638 async fn gdt_profile_line_uses_profile_of_line_symbol() -> Result<(), KclError> {
2639 let cases = [
2640 ("specific profileLine", GDT_PROFILE_LINE_KCL),
2641 ("generic profile with edges", GDT_PROFILE_GENERIC_LINE_KCL),
2642 ];
2643
2644 for (label, code) in cases {
2645 let commands = gdt_commands(code).await;
2646 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::ProfileOfLine)?;
2647
2648 assert_close(control_frame.tolerance, 0.05);
2649 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2650 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2651 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2652 }
2653 Ok(())
2654 }
2655
2656 #[tokio::test(flavor = "multi_thread")]
2657 async fn gdt_profile_surface_uses_surface_profile_symbol() -> Result<(), KclError> {
2658 let cases = [
2659 ("specific profileSurface", GDT_PROFILE_SURFACE_KCL),
2660 ("generic profile with faces", GDT_PROFILE_GENERIC_SURFACE_KCL),
2661 ];
2662
2663 for (label, code) in cases {
2664 let commands = gdt_commands(code).await;
2665 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::SurfaceProfile)?;
2666
2667 assert_close(control_frame.tolerance, 0.05);
2668 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2669 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2670 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2671 }
2672 Ok(())
2673 }
2674
2675 #[tokio::test(flavor = "multi_thread")]
2676 async fn gdt_profile_requires_edges_or_faces() {
2677 assert_eq!(
2678 parse_execute(GDT_PROFILE_MISSING_ENTITIES_KCL)
2679 .await
2680 .unwrap_err()
2681 .message(),
2682 "Profile requires either `edges` for `profileLine` or `faces` for `profileSurface`.",
2683 );
2684 }
2685
2686 #[tokio::test(flavor = "multi_thread")]
2687 async fn gdt_profile_rejects_combined_edges_and_faces() {
2688 assert_eq!(
2689 parse_execute(GDT_PROFILE_BOTH_KCL).await.unwrap_err().message(),
2690 "Profile cannot combine `edges` and `faces`. Use `profileLine` for edges or `profileSurface` for faces.",
2691 );
2692 }
2693
2694 const GDT_CIRCULARITY_EDGE_KCL: &str = r#"
2700@settings(defaultLengthUnit = mm, kclVersion = 2)
2701
2702cylinderSketch = sketch(on = XY) {
2703 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2704}
2705
2706cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
2707hide(cylinderSketch)
2708cylinder = extrude(cylinderRegion, length = 10mm)
2709gdt::circularity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm)
2710"#;
2711
2712 const GDT_CIRCULARITY_WALL_KCL: &str = r#"
2713@settings(defaultLengthUnit = mm, kclVersion = 2)
2714
2715cylinderSketch = sketch(on = XY) {
2716 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2717}
2718
2719cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
2720gdt::circularity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [12mm, 8mm], framePlane = XZ)
2721"#;
2722
2723 const GDT_CIRCULARITY_COMMON_EDGE_KCL: &str = r#"
2724@settings(defaultLengthUnit = mm, kclVersion = 2)
2725
2726cylinderSketch = sketch(on = XY) {
2727 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2728}
2729
2730cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2731topEdge = { sideFaces = [cylinder.sketch.tags.perimeter, top] }
2732gdt::circularity(edges = [topEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
2733"#;
2734
2735 #[tokio::test(flavor = "multi_thread")]
2736 async fn gdt_circularity_uses_roundness_symbol_without_datums() -> Result<(), KclError> {
2737 let cases = [
2738 ("circular edge", GDT_CIRCULARITY_EDGE_KCL, 0.05),
2739 ("cylinder wall", GDT_CIRCULARITY_WALL_KCL, 0.02),
2740 ("common edge", GDT_CIRCULARITY_COMMON_EDGE_KCL, 0.05),
2741 ];
2742
2743 for (label, code, expected_tolerance) in cases {
2744 let commands = gdt_commands(code).await;
2745 let annotation_index = new_annotation_command_index(&commands)?;
2746 let feature_control = feature_control(&commands[annotation_index])?;
2747 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
2748 KclError::new_internal(KclErrorDetails::new(
2749 format!("expected {label} feature_control to have a control_frame"),
2750 vec![SourceRange::default()],
2751 ))
2752 })?;
2753
2754 assert_eq!(control_frame.symbol, MbdSymbol::Roundness, "case: {label}");
2755 assert_close(control_frame.tolerance, expected_tolerance);
2756 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2758 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2759 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2760 }
2761 Ok(())
2762 }
2763
2764 const GDT_CYLINDRICITY_WALL_KCL: &str = r#"
2770@settings(defaultLengthUnit = mm, kclVersion = 2)
2771
2772cylinderSketch = sketch(on = XY) {
2773 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2774}
2775
2776cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
2777gdt::cylindricity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [-12mm, 8mm], framePlane = XZ)
2778"#;
2779
2780 const GDT_CYLINDRICITY_EDGE_KCL: &str = r#"
2781@settings(defaultLengthUnit = mm, kclVersion = 2)
2782
2783cylinderSketch = sketch(on = XY) {
2784 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2785}
2786
2787cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
2788hide(cylinderSketch)
2789cylinder = extrude(cylinderRegion, length = 10mm)
2790gdt::cylindricity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm, framePosition = [-12mm, 8mm])
2791"#;
2792
2793 const GDT_CYLINDRICITY_COMMON_EDGE_KCL: &str = r#"
2794@settings(defaultLengthUnit = mm, kclVersion = 2)
2795
2796cylinderSketch = sketch(on = XY) {
2797 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2798}
2799
2800cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
2801topEdge = { sideFaces = [cylinder.sketch.tags.perimeter, top] }
2802gdt::cylindricity(edges = [topEdge], tolerance = 0.05mm, framePosition = [-12mm, 8mm], framePlane = XZ)
2803"#;
2804
2805 #[tokio::test(flavor = "multi_thread")]
2806 async fn gdt_cylindricity_uses_cylindricity_symbol_without_datums() -> Result<(), KclError> {
2807 let cases = [
2808 ("cylinder wall", GDT_CYLINDRICITY_WALL_KCL, 0.02),
2809 ("circular edge", GDT_CYLINDRICITY_EDGE_KCL, 0.05),
2810 ("common edge", GDT_CYLINDRICITY_COMMON_EDGE_KCL, 0.05),
2811 ];
2812
2813 for (label, code, expected_tolerance) in cases {
2814 let commands = gdt_commands(code).await;
2815 let annotation_index = new_annotation_command_index(&commands)?;
2816 let feature_control = feature_control(&commands[annotation_index])?;
2817 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
2818 KclError::new_internal(KclErrorDetails::new(
2819 format!("expected {label} feature_control to have a control_frame"),
2820 vec![SourceRange::default()],
2821 ))
2822 })?;
2823
2824 assert_eq!(control_frame.symbol, MbdSymbol::Cylindricity, "case: {label}");
2825 assert_close(control_frame.tolerance, expected_tolerance);
2826 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2828 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2829 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2830 }
2831 Ok(())
2832 }
2833
2834 const GDT_CONCENTRICITY_REFERENCE_FEATURE_B_FACE_KCL: &str = r#"
2839@settings(defaultLengthUnit = mm, kclVersion = 2)
2840
2841datumASketch = sketch(on = XY) {
2842 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2843}
2844
2845datumA = extrude(region(point = datumASketch.perimeter.center, sketch = datumASketch), length = 16mm)
2846
2847referenceFeatureBSketch = sketch(on = XY) {
2848 perimeter = circle(start = [var 2.5mm, var 0mm], center = [var 0mm, var 0mm])
2849}
2850
2851referenceFeatureB = extrude(region(point = referenceFeatureBSketch.perimeter.center, sketch = referenceFeatureBSketch), length = 12mm)
2852 |> translate(z = -12mm)
2853
2854gdt::datum(face = datumA.sketch.tags.perimeter, name = "A", framePosition = [10mm, -12mm], framePlane = XZ)
2855gdt::concentricity(faces = [referenceFeatureB.sketch.tags.perimeter], tolerance = 0.2mm, datums = ["A"], framePosition = [-18mm, 12mm], framePlane = XZ)
2856"#;
2857
2858 const GDT_CONCENTRICITY_REFERENCE_FEATURE_B_EDGE_KCL: &str = r#"
2859@settings(defaultLengthUnit = mm, kclVersion = 2)
2860
2861datumASketch = sketch(on = XY) {
2862 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
2863}
2864
2865datumA = extrude(region(point = datumASketch.perimeter.center, sketch = datumASketch), length = 16mm)
2866
2867referenceFeatureBSketch = sketch(on = XY) {
2868 perimeter = circle(start = [var 2.5mm, var 0mm], center = [var 0mm, var 0mm])
2869}
2870
2871referenceFeatureB = extrude(region(point = referenceFeatureBSketch.perimeter.center, sketch = referenceFeatureBSketch), length = 12mm, tagEnd = $endB)
2872 |> translate(z = -12mm)
2873endEdgeB = { sideFaces = [referenceFeatureB.sketch.tags.perimeter, endB] }
2874
2875gdt::datum(face = datumA.sketch.tags.perimeter, name = "A", framePosition = [10mm, -12mm], framePlane = XZ)
2876gdt::concentricity(edges = [endEdgeB], tolerance = 0.2mm, datums = ["A"], framePosition = [-18mm, 12mm], framePlane = XZ)
2877"#;
2878
2879 #[tokio::test(flavor = "multi_thread")]
2880 async fn gdt_concentricity_uses_concentricity_symbol_with_diameter_zone_and_datums() -> Result<(), KclError> {
2881 let cases = [
2882 (
2883 "reference feature B face",
2884 GDT_CONCENTRICITY_REFERENCE_FEATURE_B_FACE_KCL,
2885 0.2,
2886 ),
2887 (
2888 "reference feature B edge",
2889 GDT_CONCENTRICITY_REFERENCE_FEATURE_B_EDGE_KCL,
2890 0.2,
2891 ),
2892 ];
2893
2894 for (label, code, expected_tolerance) in cases {
2895 let commands = gdt_commands(code).await;
2896 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Concentricity)?;
2897
2898 assert_eq!(
2899 control_frame.diameter_symbol,
2900 Some(MbdSymbol::Diameter),
2901 "case: {label}"
2902 );
2903 assert_close(control_frame.tolerance, expected_tolerance);
2904 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
2905 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2906 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2907 }
2908 Ok(())
2909 }
2910
2911 const GDT_SYMMETRY_LATCH_BLOCK_GROOVE_FACE_KCL: &str = r#"
2915@settings(defaultLengthUnit = mm, kclVersion = 2)
2916
2917latchProfile = sketch(on = XZ) {
2918 bottom = line(start = [var -20mm, var -10mm], end = [var 20mm, var -10mm])
2919 datumWidthFace = line(start = [var 20mm, var -10mm], end = [var 20mm, var 10mm])
2920 topRight = line(start = [var 20mm, var 10mm], end = [var 5mm, var 10mm])
2921 rightGrooveWall = line(start = [var 5mm, var 10mm], end = [var 5mm, var 3mm])
2922 grooveFloor = line(start = [var 5mm, var 3mm], end = [var -5mm, var 3mm])
2923 leftGrooveWall = line(start = [var -5mm, var 3mm], end = [var -5mm, var 10mm])
2924 topLeft = line(start = [var -5mm, var 10mm], end = [var -20mm, var 10mm])
2925 leftSide = line(start = [var -20mm, var 10mm], end = [var -20mm, var -10mm])
2926 coincident([bottom.end, datumWidthFace.start])
2927 coincident([datumWidthFace.end, topRight.start])
2928 coincident([topRight.end, rightGrooveWall.start])
2929 coincident([rightGrooveWall.end, grooveFloor.start])
2930 coincident([grooveFloor.end, leftGrooveWall.start])
2931 coincident([leftGrooveWall.end, topLeft.start])
2932 coincident([topLeft.end, leftSide.start])
2933 coincident([leftSide.end, bottom.start])
2934 horizontal(bottom)
2935 vertical(datumWidthFace)
2936 horizontal(topRight)
2937 vertical(rightGrooveWall)
2938 horizontal(grooveFloor)
2939 vertical(leftGrooveWall)
2940 horizontal(topLeft)
2941 vertical(leftSide)
2942}
2943
2944latchBlockRegion = region(point = [0mm, 0mm], sketch = latchProfile)
2945latchBlock = extrude(latchBlockRegion, length = 12mm)
2946
2947gdt::datum(face = latchBlock.sketch.tags.bottom, name = "A", framePosition = [0mm, -16mm], framePlane = XZ)
2948gdt::symmetry(faces = [latchBlock.sketch.tags.grooveFloor], tolerance = 0.2mm, datums = ["A"], framePosition = [-24mm, 14mm], framePlane = XZ)
2949"#;
2950
2951 const GDT_SYMMETRY_LATCH_BLOCK_GROOVE_EDGE_KCL: &str = r#"
2952@settings(defaultLengthUnit = mm, kclVersion = 2)
2953
2954latchProfile = sketch(on = XZ) {
2955 bottom = line(start = [var -20mm, var -10mm], end = [var 20mm, var -10mm])
2956 datumWidthFace = line(start = [var 20mm, var -10mm], end = [var 20mm, var 10mm])
2957 topRight = line(start = [var 20mm, var 10mm], end = [var 5mm, var 10mm])
2958 rightGrooveWall = line(start = [var 5mm, var 10mm], end = [var 5mm, var 3mm])
2959 grooveFloor = line(start = [var 5mm, var 3mm], end = [var -5mm, var 3mm])
2960 leftGrooveWall = line(start = [var -5mm, var 3mm], end = [var -5mm, var 10mm])
2961 topLeft = line(start = [var -5mm, var 10mm], end = [var -20mm, var 10mm])
2962 leftSide = line(start = [var -20mm, var 10mm], end = [var -20mm, var -10mm])
2963 coincident([bottom.end, datumWidthFace.start])
2964 coincident([datumWidthFace.end, topRight.start])
2965 coincident([topRight.end, rightGrooveWall.start])
2966 coincident([rightGrooveWall.end, grooveFloor.start])
2967 coincident([grooveFloor.end, leftGrooveWall.start])
2968 coincident([leftGrooveWall.end, topLeft.start])
2969 coincident([topLeft.end, leftSide.start])
2970 coincident([leftSide.end, bottom.start])
2971 horizontal(bottom)
2972 vertical(datumWidthFace)
2973 horizontal(topRight)
2974 vertical(rightGrooveWall)
2975 horizontal(grooveFloor)
2976 vertical(leftGrooveWall)
2977 horizontal(topLeft)
2978 vertical(leftSide)
2979}
2980
2981latchBlockRegion = region(point = [0mm, 0mm], sketch = latchProfile)
2982latchBlock = extrude(latchBlockRegion, length = 12mm, tagEnd = $frontFace)
2983grooveFloorFrontEdge = { sideFaces = [latchBlock.sketch.tags.grooveFloor, frontFace] }
2984
2985gdt::datum(face = latchBlock.sketch.tags.bottom, name = "A", framePosition = [0mm, -16mm], framePlane = XZ)
2986gdt::symmetry(edges = [grooveFloorFrontEdge], tolerance = 0.2mm, datums = ["A"], framePosition = [-24mm, 14mm], framePlane = XZ)
2987"#;
2988
2989 #[tokio::test(flavor = "multi_thread")]
2990 async fn gdt_symmetry_uses_symmetry_symbol_with_datums_for_face() -> Result<(), KclError> {
2991 let commands = gdt_commands(GDT_SYMMETRY_LATCH_BLOCK_GROOVE_FACE_KCL).await;
2992 let control_frames: Vec<_> = commands
2993 .iter()
2994 .filter_map(|command| {
2995 feature_control(command)
2996 .ok()
2997 .and_then(|feature_control| feature_control.control_frame.as_ref())
2998 .filter(|control_frame| control_frame.symbol == MbdSymbol::Symmetry)
2999 })
3000 .collect();
3001
3002 assert_eq!(control_frames.len(), 1);
3003 let control_frame = control_frames[0];
3004 assert_eq!(control_frame.diameter_symbol, None);
3005 assert_close(control_frame.tolerance, 0.2);
3006 assert_eq!(control_frame.primary_datum, Some('A'));
3007 assert!(control_frame.secondary_datum.is_none());
3008 assert!(control_frame.tertiary_datum.is_none());
3009 Ok(())
3010 }
3011
3012 #[tokio::test(flavor = "multi_thread")]
3013 async fn gdt_symmetry_uses_symmetry_symbol_with_datums_for_edge() -> Result<(), KclError> {
3014 let commands = gdt_commands(GDT_SYMMETRY_LATCH_BLOCK_GROOVE_EDGE_KCL).await;
3015 let control_frames: Vec<_> = commands
3016 .iter()
3017 .filter_map(|command| {
3018 feature_control(command)
3019 .ok()
3020 .and_then(|feature_control| feature_control.control_frame.as_ref())
3021 .filter(|control_frame| control_frame.symbol == MbdSymbol::Symmetry)
3022 })
3023 .collect();
3024
3025 assert_eq!(control_frames.len(), 1);
3026 let control_frame = control_frames[0];
3027 assert_eq!(control_frame.diameter_symbol, None);
3028 assert_close(control_frame.tolerance, 0.2);
3029 assert_eq!(control_frame.primary_datum, Some('A'));
3030 assert!(control_frame.secondary_datum.is_none());
3031 assert!(control_frame.tertiary_datum.is_none());
3032 Ok(())
3033 }
3034
3035 const GDT_RUNOUT_STEPPED_SHAFT_KCL: &str = r#"
3039@settings(defaultLengthUnit = mm, kclVersion = 2)
3040
3041annotationPlane = offsetPlane(XZ, offset = 24mm)
3042
3043controlledSketch = sketch(on = YZ) {
3044 upperPerimeter = arc(start = [var 10mm, var 0mm], end = [var -10mm, var 0mm], center = [var 0mm, var 0mm])
3045 lowerPerimeter = arc(start = [var -10mm, var 0mm], end = [var 10mm, var 0mm], center = [var 0mm, var 0mm])
3046 coincident([upperPerimeter.end, lowerPerimeter.start])
3047 coincident([lowerPerimeter.end, upperPerimeter.start])
3048}
3049
3050controlledShaft = extrude(
3051 region(point = [0mm, 1mm], sketch = controlledSketch),
3052 length = -58mm,
3053 tagStart = $controlledShoulder,
3054 tagEnd = $controlledFreeEnd
3055)
3056
3057controlledUpperShoulderEdge = { sideFaces = [
3058 controlledShaft.sketch.tags.upperPerimeter,
3059 controlledShoulder
3060] }
3061
3062datumSketch = sketch(on = YZ) {
3063 perimeter = circle(start = [var 18mm, var 0mm], center = [var 0mm, var 0mm])
3064}
3065
3066datumShaft = extrude(
3067 region(point = datumSketch.perimeter.center, sketch = datumSketch),
3068 length = 36mm,
3069 tagEnd = $datumEnd
3070)
3071
3072gdt::datum(
3073 face = datumShaft.sketch.tags.perimeter,
3074 name = "A",
3075 framePosition = [18mm, -28mm],
3076 framePlane = annotationPlane,
3077 leaderScale = 1.15,
3078 fontSize = 6mm
3079)
3080
3081gdt::runout(
3082 edges = [controlledUpperShoulderEdge],
3083 tolerance = 0.2mm,
3084 datums = ["A"],
3085 precision = 1,
3086 framePosition = [12mm, 48mm],
3087 framePlane = annotationPlane,
3088 leaderScale = 1.15,
3089 fontSize = 6mm
3090)
3091"#;
3092
3093 const GDT_RUNOUT_FACE_KCL: &str = r#"
3094@settings(defaultLengthUnit = mm, kclVersion = 2)
3095
3096datumSketch = sketch(on = XY) {
3097 perimeter = circle(start = [var 6mm, var 0mm], center = [var 0mm, var 0mm])
3098}
3099
3100datumShaft = extrude(region(point = datumSketch.perimeter.center, sketch = datumSketch), length = 18mm)
3101
3102controlledSketch = sketch(on = XY) {
3103 perimeter = circle(start = [var 3mm, var 0mm], center = [var 0mm, var 0mm])
3104}
3105
3106controlledShaft = extrude(region(point = controlledSketch.perimeter.center, sketch = controlledSketch), length = 16mm)
3107 |> translate(z = -16mm)
3108
3109gdt::datum(face = datumShaft.sketch.tags.perimeter, name = "A", framePosition = [12mm, -14mm], framePlane = XZ)
3110gdt::runout(faces = [controlledShaft.sketch.tags.perimeter], tolerance = 0.2mm, datums = ["A"], framePosition = [-18mm, 12mm], framePlane = XZ)
3111"#;
3112
3113 #[tokio::test(flavor = "multi_thread")]
3114 async fn gdt_runout_uses_runout_symbol_with_axis_datum() -> Result<(), KclError> {
3115 let cases = [
3116 ("stepped shaft", GDT_RUNOUT_STEPPED_SHAFT_KCL, 0.2),
3117 ("controlled face", GDT_RUNOUT_FACE_KCL, 0.2),
3118 ];
3119
3120 for (label, code, expected_tolerance) in cases {
3121 let commands = gdt_commands(code).await;
3122 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Runout)?;
3123
3124 assert!(control_frame.diameter_symbol.is_none(), "case: {label}");
3125 assert_close(control_frame.tolerance, expected_tolerance);
3126 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
3127 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
3128 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
3129 }
3130 Ok(())
3131 }
3132}