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