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