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