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