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