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 Profile,
118 Position,
119 Angularity,
120 Perpendicularity,
121 Parallelism,
122}
123
124struct GdtFeatureControlParams {
125 faces: Vec<TagIdentifier>,
126 edges: Vec<EdgeReference>,
127 datums: Option<Vec<String>>,
128 tolerance: TyF64,
129 precision: Option<TyF64>,
130 frame_position: Option<[TyF64; 2]>,
131 frame_plane: Option<Plane>,
132 leader_scale: Option<TyF64>,
133 font_size: Option<TyF64>,
134}
135
136impl GdtFeatureControlKind {
137 fn label(self) -> &'static str {
138 match self {
139 Self::Flatness => "Flatness",
140 Self::Straightness => "Straightness",
141 Self::Circularity => "Circularity",
142 Self::Cylindricity => "Cylindricity",
143 Self::Profile => "Profile",
144 Self::Position => "Position",
145 Self::Angularity => "Angularity",
146 Self::Perpendicularity => "Perpendicularity",
147 Self::Parallelism => "Parallelism",
148 }
149 }
150
151 fn symbol(self) -> MbdSymbol {
152 match self {
153 Self::Flatness => MbdSymbol::Flatness,
154 Self::Straightness => MbdSymbol::Straightness,
155 Self::Circularity => MbdSymbol::Roundness,
156 Self::Cylindricity => MbdSymbol::Cylindricity,
157 Self::Profile => MbdSymbol::ProfileOfLine,
158 Self::Position => MbdSymbol::Position,
159 Self::Angularity => MbdSymbol::Angularity,
160 Self::Perpendicularity => MbdSymbol::Perpendicularity,
161 Self::Parallelism => MbdSymbol::Parallelism,
162 }
163 }
164}
165
166fn add_gdt_annotation_artifact(exec_state: &mut ExecState, args: &Args, annotation_id: uuid::Uuid) {
167 exec_state.add_artifact(Artifact::GdtAnnotation(GdtAnnotationArtifact {
168 id: ArtifactId::new(annotation_id),
169 code_ref: CodeRef::placeholder(args.source_range),
170 }));
171}
172
173impl DistanceEntity {
174 async fn to_endpoint(&self, exec_state: &mut ExecState, args: &Args) -> Result<DistanceEndpoint, KclError> {
175 match self {
176 DistanceEntity::Face(face) => Ok(DistanceEndpoint {
177 entity_id: face.id,
178 entity_pos: KPoint2d { x: 0.5, y: 0.5 },
179 }),
180 DistanceEntity::TaggedFace(face) => Ok(DistanceEndpoint {
181 entity_id: args.get_adjacent_face_to_tag(exec_state, face, false).await?,
182 entity_pos: KPoint2d { x: 0.5, y: 0.5 },
183 }),
184 DistanceEntity::Edge(edge) => Ok(DistanceEndpoint {
185 entity_id: edge.get_engine_id(exec_state, args)?,
186 entity_pos: KPoint2d { x: 0.5, y: 0.0 },
187 }),
188 }
189 }
190}
191
192impl<'a> FromKclValue<'a> for DistanceEntity {
193 fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
194 match arg {
195 KclValue::Face { value } => Some(Self::Face(value.to_owned())),
196 KclValue::Uuid { value, .. } => Some(Self::Edge(EdgeReference::Uuid(*value))),
197 KclValue::TagIdentifier(value) => Some(Self::TaggedFace(value.to_owned())),
198 _ => None,
199 }
200 }
201}
202
203fn distance_entity_type() -> RuntimeType {
204 RuntimeType::Union(vec![
205 RuntimeType::face(),
206 RuntimeType::tagged_face(),
207 RuntimeType::edge(),
208 ])
209}
210
211pub async fn datum(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
212 let face: TagIdentifier = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
213 let name: String = args.get_kw_arg("name", &RuntimeType::string(), exec_state)?;
214 let frame_position: Option<[TyF64; 2]> =
215 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
216 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
217 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
218 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
219
220 let annotation = inner_datum(
221 face,
222 name,
223 frame_position,
224 frame_plane,
225 leader_scale,
226 font_size,
227 exec_state,
228 &args,
229 )
230 .await?;
231 Ok(KclValue::GdtAnnotation {
232 value: Box::new(annotation),
233 })
234}
235
236#[allow(clippy::too_many_arguments)]
237async fn inner_datum(
238 face: TagIdentifier,
239 name: String,
240 frame_position: Option<[TyF64; 2]>,
241 frame_plane: Option<Plane>,
242 leader_scale: Option<TyF64>,
243 font_size: Option<TyF64>,
244 exec_state: &mut ExecState,
245 args: &Args,
246) -> Result<GdtAnnotation, KclError> {
247 const DATUM_LENGTH_ERROR: &str = "Datum name must be a single character.";
248 if name.len() > 1 {
249 return Err(KclError::new_semantic(KclErrorDetails::new(
250 DATUM_LENGTH_ERROR.to_owned(),
251 vec![args.source_range],
252 )));
253 }
254 let name_char = name.chars().next().ok_or_else(|| {
255 KclError::new_semantic(KclErrorDetails::new(
256 DATUM_LENGTH_ERROR.to_owned(),
257 vec![args.source_range],
258 ))
259 })?;
260 let mut frame_plane = if let Some(plane) = frame_plane {
261 plane
262 } else {
263 xy_plane(exec_state, args).await?
265 };
266 ensure_sketch_plane_in_engine(
267 &mut frame_plane,
268 exec_state,
269 &args.ctx,
270 args.source_range,
271 args.node_path.clone(),
272 )
273 .await?;
274 let face_id = args.get_adjacent_face_to_tag(exec_state, &face, false).await?;
275 let meta = vec![Metadata::from(args.source_range)];
276 let annotation_id = exec_state.next_uuid();
277 let feature_control = AnnotationFeatureControl::builder()
278 .entity_id(face_id)
279 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
281 .leader_type(AnnotationLineEnd::Dot)
282 .defined_datum(name_char)
283 .plane_id(frame_plane.id)
284 .offset(if let Some(offset) = &frame_position {
285 KPoint2d {
286 x: offset[0].to_mm(),
287 y: offset[1].to_mm(),
288 }
289 } else {
290 KPoint2d { x: 100.0, y: 100.0 }
291 })
292 .precision(0)
293 .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
294 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
295 .leader_scale(gdt_dot_leader_scale(leader_scale.as_ref(), font_size.as_ref(), args)?)
296 .build();
297 exec_state
298 .batch_modeling_cmd(
299 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
300 ModelingCmd::from(
301 mcmd::NewAnnotation::builder()
302 .options(AnnotationOptions::builder().feature_control(feature_control).build())
303 .clobber(false)
304 .annotation_type(AnnotationType::T3D)
305 .build(),
306 ),
307 )
308 .await?;
309 add_gdt_annotation_artifact(exec_state, args, annotation_id);
310 Ok(GdtAnnotation {
311 id: annotation_id,
312 meta,
313 })
314}
315
316pub async fn flatness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
317 let faces: Vec<TagIdentifier> = args.get_kw_arg(
318 "faces",
319 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
320 exec_state,
321 )?;
322 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
323 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
324 let frame_position: Option<[TyF64; 2]> =
325 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
326 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
327 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
328 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
329
330 let annotations = create_feature_control_annotations(
331 GdtFeatureControlKind::Flatness,
332 GdtFeatureControlParams {
333 faces,
334 edges: Vec::new(),
335 datums: None,
336 tolerance,
337 precision,
338 frame_position,
339 frame_plane,
340 leader_scale,
341 font_size,
342 },
343 exec_state,
344 &args,
345 )
346 .await?;
347 Ok(annotations.into())
348}
349
350pub async fn straightness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
351 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
352 "faces",
353 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
354 exec_state,
355 )?;
356 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
357 "edges",
358 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
359 exec_state,
360 )?;
361 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
362 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
363 let frame_position: Option<[TyF64; 2]> =
364 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
365 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
366 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
367 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
368
369 let annotations = create_feature_control_annotations(
370 GdtFeatureControlKind::Straightness,
371 GdtFeatureControlParams {
372 faces: faces.unwrap_or_default(),
373 edges: edges.unwrap_or_default(),
374 datums: None,
375 tolerance,
376 precision,
377 frame_position,
378 frame_plane,
379 leader_scale,
380 font_size,
381 },
382 exec_state,
383 &args,
384 )
385 .await?;
386 Ok(annotations.into())
387}
388
389pub async fn circularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
390 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
391 "faces",
392 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
393 exec_state,
394 )?;
395 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
396 "edges",
397 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
398 exec_state,
399 )?;
400 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
401 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
402 let frame_position: Option<[TyF64; 2]> =
403 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
404 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
405 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
406 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
407
408 let annotations = create_feature_control_annotations(
409 GdtFeatureControlKind::Circularity,
410 GdtFeatureControlParams {
411 faces: faces.unwrap_or_default(),
412 edges: edges.unwrap_or_default(),
413 datums: None,
414 tolerance,
415 precision,
416 frame_position,
417 frame_plane,
418 leader_scale,
419 font_size,
420 },
421 exec_state,
422 &args,
423 )
424 .await?;
425 Ok(annotations.into())
426}
427
428pub async fn cylindricity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
429 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
430 "faces",
431 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
432 exec_state,
433 )?;
434 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
435 "edges",
436 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
437 exec_state,
438 )?;
439 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
440 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
441 let frame_position: Option<[TyF64; 2]> =
442 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
443 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
444 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
445 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
446
447 let annotations = create_feature_control_annotations(
448 GdtFeatureControlKind::Cylindricity,
449 GdtFeatureControlParams {
450 faces: faces.unwrap_or_default(),
451 edges: edges.unwrap_or_default(),
452 datums: None,
453 tolerance,
454 precision,
455 frame_position,
456 frame_plane,
457 leader_scale,
458 font_size,
459 },
460 exec_state,
461 &args,
462 )
463 .await?;
464 Ok(annotations.into())
465}
466
467pub async fn profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
468 let edges: Vec<EdgeReference> = args.get_kw_arg(
469 "edges",
470 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
471 exec_state,
472 )?;
473 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
474 "datums",
475 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
476 exec_state,
477 )?;
478 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
479 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
480 let frame_position: Option<[TyF64; 2]> =
481 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
482 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
483 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
484 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
485
486 let annotations = create_feature_control_annotations(
487 GdtFeatureControlKind::Profile,
488 GdtFeatureControlParams {
489 faces: Vec::new(),
490 edges,
491 datums,
492 tolerance,
493 precision,
494 frame_position,
495 frame_plane,
496 leader_scale,
497 font_size,
498 },
499 exec_state,
500 &args,
501 )
502 .await?;
503 Ok(annotations.into())
504}
505
506pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
507 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
508 "faces",
509 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
510 exec_state,
511 )?;
512 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
513 "edges",
514 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
515 exec_state,
516 )?;
517 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
518 "datums",
519 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
520 exec_state,
521 )?;
522 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
523 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
524 let frame_position: Option<[TyF64; 2]> =
525 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
526 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
527 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
528 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
529
530 let annotations = create_feature_control_annotations(
531 GdtFeatureControlKind::Position,
532 GdtFeatureControlParams {
533 faces: faces.unwrap_or_default(),
534 edges: edges.unwrap_or_default(),
535 datums,
536 tolerance,
537 precision,
538 frame_position,
539 frame_plane,
540 leader_scale,
541 font_size,
542 },
543 exec_state,
544 &args,
545 )
546 .await?;
547 Ok(annotations.into())
548}
549
550pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
551 let from: Option<DistanceEntity> = args.get_kw_arg_opt("from", &distance_entity_type(), exec_state)?;
552 let to: Option<DistanceEntity> = args.get_kw_arg_opt("to", &distance_entity_type(), exec_state)?;
553 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
554 "edges",
555 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
556 exec_state,
557 )?;
558 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
559 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
560 let frame_position: Option<[TyF64; 2]> =
561 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
562 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
563 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
564 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
565
566 let annotations = inner_distance(
567 from,
568 to,
569 edges.unwrap_or_default(),
570 tolerance,
571 precision,
572 frame_position,
573 frame_plane,
574 leader_scale,
575 font_size,
576 exec_state,
577 &args,
578 )
579 .await?;
580 Ok(annotations.into())
581}
582
583#[allow(clippy::too_many_arguments)]
584async fn inner_distance(
585 from: Option<DistanceEntity>,
586 to: Option<DistanceEntity>,
587 edges: Vec<EdgeReference>,
588 tolerance: TyF64,
589 precision: Option<TyF64>,
590 frame_position: Option<[TyF64; 2]>,
591 frame_plane: Option<Plane>,
592 leader_scale: Option<TyF64>,
593 font_size: Option<TyF64>,
594 exec_state: &mut ExecState,
595 args: &Args,
596) -> Result<Vec<GdtAnnotation>, KclError> {
597 let precision = resolve_precision(precision, args)?;
598 let mut frame_plane = if let Some(plane) = frame_plane {
599 plane
600 } else {
601 xy_plane(exec_state, args).await?
602 };
603 ensure_sketch_plane_in_engine(
604 &mut frame_plane,
605 exec_state,
606 &args.ctx,
607 args.source_range,
608 args.node_path.clone(),
609 )
610 .await?;
611
612 if from.is_some() || to.is_some() {
613 if !edges.is_empty() {
614 return Err(KclError::new_semantic(KclErrorDetails::new(
615 "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
616 vec![args.source_range],
617 )));
618 }
619
620 let (Some(from), Some(to)) = (from, to) else {
621 return Err(KclError::new_semantic(KclErrorDetails::new(
622 "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
623 vec![args.source_range],
624 )));
625 };
626
627 let from = from.to_endpoint(exec_state, args).await?;
628 let to = to.to_endpoint(exec_state, args).await?;
629 let mut annotations = Vec::with_capacity(1);
630 create_basic_distance_annotation(
631 from,
632 to,
633 &tolerance,
634 precision,
635 frame_position.as_ref(),
636 frame_plane.id,
637 leader_scale.as_ref(),
638 font_size.as_ref(),
639 exec_state,
640 args,
641 &mut annotations,
642 )
643 .await?;
644 return Ok(annotations);
645 }
646
647 if edges.is_empty() {
648 return Err(KclError::new_semantic(KclErrorDetails::new(
649 "Distance requires either `edges` or both `from` and `to`.".to_owned(),
650 vec![args.source_range],
651 )));
652 }
653
654 let mut annotations = Vec::with_capacity(edges.len());
655 for edge in &edges {
656 let edge_id = edge.get_engine_id(exec_state, args)?;
657 create_basic_distance_annotation(
658 DistanceEndpoint {
659 entity_id: edge_id,
660 entity_pos: KPoint2d { x: 0.0, y: 0.0 },
661 },
662 DistanceEndpoint {
663 entity_id: edge_id,
664 entity_pos: KPoint2d { x: 1.0, y: 0.0 },
665 },
666 &tolerance,
667 precision,
668 frame_position.as_ref(),
669 frame_plane.id,
670 leader_scale.as_ref(),
671 font_size.as_ref(),
672 exec_state,
673 args,
674 &mut annotations,
675 )
676 .await?;
677 }
678 Ok(annotations)
679}
680
681#[allow(clippy::too_many_arguments)]
682async fn create_basic_distance_annotation(
683 from: DistanceEndpoint,
684 to: DistanceEndpoint,
685 tolerance: &TyF64,
686 precision: u32,
687 frame_position: Option<&[TyF64; 2]>,
688 frame_plane_id: uuid::Uuid,
689 leader_scale: Option<&TyF64>,
690 font_size: Option<&TyF64>,
691 exec_state: &mut ExecState,
692 args: &Args,
693 annotations: &mut Vec<GdtAnnotation>,
694) -> Result<(), KclError> {
695 let meta = vec![Metadata::from(args.source_range)];
696 let annotation_id = exec_state.next_uuid();
697 let display_units = exec_state.length_unit();
698 let dimension = AnnotationBasicDimension::builder()
699 .from_entity_id(from.entity_id)
700 .from_entity_pos(from.entity_pos)
701 .to_entity_id(to.entity_id)
702 .to_entity_pos(to.entity_pos)
703 .dimension(
704 AnnotationMbdBasicDimension::builder()
705 .tolerance(tolerance.to_length_units(display_units))
706 .build(),
707 )
708 .plane_id(frame_plane_id)
709 .offset(if let Some(offset) = frame_position {
710 KPoint2d {
711 x: offset[0].to_mm(),
712 y: offset[1].to_mm(),
713 }
714 } else {
715 KPoint2d { x: 100.0, y: 100.0 }
716 })
717 .precision(precision)
718 .font_scale(gdt_font_scale(font_size, args)?)
719 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
720 .arrow_scale(gdt_dimension_leader_scale(leader_scale, args)?)
721 .build();
722 let options = AnnotationOptions::builder()
723 .dimension(dimension)
724 .units(display_units)
725 .build();
726 let annotation_cmd = ModelingCmd::from(
727 mcmd::NewAnnotation::builder()
728 .options(options)
729 .clobber(false)
730 .annotation_type(AnnotationType::T3D)
731 .build(),
732 );
733 let cmd_meta = ModelingCmdMeta::from_args_id(exec_state, args, annotation_id);
734 exec_state.batch_modeling_cmd(cmd_meta, annotation_cmd).await?;
735 add_gdt_annotation_artifact(exec_state, args, annotation_id);
736 annotations.push(GdtAnnotation {
737 id: annotation_id,
738 meta,
739 });
740 Ok(())
741}
742
743pub async fn angularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
744 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
745 "faces",
746 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
747 exec_state,
748 )?;
749 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
750 "edges",
751 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
752 exec_state,
753 )?;
754 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
755 "datums",
756 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
757 exec_state,
758 )?;
759 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
760 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
761 let frame_position: Option<[TyF64; 2]> =
762 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
763 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
764 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
765 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
766
767 let annotations = create_feature_control_annotations(
768 GdtFeatureControlKind::Angularity,
769 GdtFeatureControlParams {
770 faces: faces.unwrap_or_default(),
771 edges: edges.unwrap_or_default(),
772 datums,
773 tolerance,
774 precision,
775 frame_position,
776 frame_plane,
777 leader_scale,
778 font_size,
779 },
780 exec_state,
781 &args,
782 )
783 .await?;
784 Ok(annotations.into())
785}
786
787pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
788 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
789 "faces",
790 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
791 exec_state,
792 )?;
793 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
794 "edges",
795 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
796 exec_state,
797 )?;
798 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
799 "datums",
800 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
801 exec_state,
802 )?;
803 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
804 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
805 let frame_position: Option<[TyF64; 2]> =
806 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
807 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
808 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
809 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
810
811 let annotations = create_feature_control_annotations(
812 GdtFeatureControlKind::Perpendicularity,
813 GdtFeatureControlParams {
814 faces: faces.unwrap_or_default(),
815 edges: edges.unwrap_or_default(),
816 datums,
817 tolerance,
818 precision,
819 frame_position,
820 frame_plane,
821 leader_scale,
822 font_size,
823 },
824 exec_state,
825 &args,
826 )
827 .await?;
828 Ok(annotations.into())
829}
830
831pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
832 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
833 "faces",
834 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
835 exec_state,
836 )?;
837 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
838 "edges",
839 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
840 exec_state,
841 )?;
842 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
843 "datums",
844 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
845 exec_state,
846 )?;
847 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
848 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
849 let frame_position: Option<[TyF64; 2]> =
850 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
851 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
852 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
853 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
854
855 let annotations = create_feature_control_annotations(
856 GdtFeatureControlKind::Parallelism,
857 GdtFeatureControlParams {
858 faces: faces.unwrap_or_default(),
859 edges: edges.unwrap_or_default(),
860 datums,
861 tolerance,
862 precision,
863 frame_position,
864 frame_plane,
865 leader_scale,
866 font_size,
867 },
868 exec_state,
869 &args,
870 )
871 .await?;
872 Ok(annotations.into())
873}
874
875pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
876 let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
877 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
878 "faces",
879 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
880 exec_state,
881 )?;
882 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
883 "edges",
884 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
885 exec_state,
886 )?;
887 let frame_position: Option<[TyF64; 2]> =
888 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
889 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
890 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
891 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
892
893 let annotations = inner_annotation(
894 annotation,
895 faces.unwrap_or_default(),
896 edges.unwrap_or_default(),
897 frame_position,
898 frame_plane,
899 leader_scale,
900 font_size,
901 exec_state,
902 &args,
903 )
904 .await?;
905 Ok(annotations.into())
906}
907
908#[allow(clippy::too_many_arguments)]
909async fn inner_annotation(
910 annotation: String,
911 faces: Vec<TagIdentifier>,
912 edges: Vec<EdgeReference>,
913 frame_position: Option<[TyF64; 2]>,
914 frame_plane: Option<Plane>,
915 leader_scale: Option<TyF64>,
916 font_size: Option<TyF64>,
917 exec_state: &mut ExecState,
918 args: &Args,
919) -> Result<Vec<GdtAnnotation>, KclError> {
920 if annotation.is_empty() {
921 return Err(KclError::new_semantic(KclErrorDetails::new(
922 "Annotation text must not be empty.".to_owned(),
923 vec![args.source_range],
924 )));
925 }
926 if faces.is_empty() && edges.is_empty() {
927 return Err(KclError::new_semantic(KclErrorDetails::new(
928 "Annotation requires at least one face or edge.".to_owned(),
929 vec![args.source_range],
930 )));
931 }
932
933 let mut frame_plane = if let Some(plane) = frame_plane {
934 plane
935 } else {
936 xy_plane(exec_state, args).await?
937 };
938 ensure_sketch_plane_in_engine(
939 &mut frame_plane,
940 exec_state,
941 &args.ctx,
942 args.source_range,
943 args.node_path.clone(),
944 )
945 .await?;
946
947 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
948 for face in &faces {
949 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
950 create_annotation(
951 face_id,
952 &annotation,
953 frame_position.as_ref(),
954 frame_plane.id,
955 leader_scale.as_ref(),
956 font_size.as_ref(),
957 exec_state,
958 args,
959 &mut annotations,
960 )
961 .await?;
962 }
963 for edge in &edges {
964 let edge_id = edge.get_engine_id(exec_state, args)?;
965 create_annotation(
966 edge_id,
967 &annotation,
968 frame_position.as_ref(),
969 frame_plane.id,
970 leader_scale.as_ref(),
971 font_size.as_ref(),
972 exec_state,
973 args,
974 &mut annotations,
975 )
976 .await?;
977 }
978
979 Ok(annotations)
980}
981
982fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
983 if let Some(precision) = precision {
984 let rounded = precision.n.round();
985 if !(0.0..=9.0).contains(&rounded) {
986 return Err(KclError::new_semantic(KclErrorDetails::new(
987 "Precision must be between 0 and 9".to_owned(),
988 vec![args.source_range],
989 )));
990 }
991 Ok(rounded as u32)
992 } else {
993 Ok(3)
994 }
995}
996
997async fn resolve_gdt_frame_plane(
998 frame_plane: Option<Plane>,
999 exec_state: &mut ExecState,
1000 args: &Args,
1001) -> Result<Plane, KclError> {
1002 let mut frame_plane = if let Some(plane) = frame_plane {
1003 plane
1004 } else {
1005 xy_plane(exec_state, args).await?
1007 };
1008 ensure_sketch_plane_in_engine(
1009 &mut frame_plane,
1010 exec_state,
1011 &args.ctx,
1012 args.source_range,
1013 args.node_path.clone(),
1014 )
1015 .await?;
1016 Ok(frame_plane)
1017}
1018
1019async fn create_feature_control_annotations(
1020 kind: GdtFeatureControlKind,
1021 params: GdtFeatureControlParams,
1022 exec_state: &mut ExecState,
1023 args: &Args,
1024) -> Result<Vec<GdtAnnotation>, KclError> {
1025 let GdtFeatureControlParams {
1026 faces,
1027 edges,
1028 datums,
1029 tolerance,
1030 precision,
1031 frame_position,
1032 frame_plane,
1033 leader_scale,
1034 font_size,
1035 } = params;
1036
1037 if faces.is_empty() && edges.is_empty() {
1038 return Err(KclError::new_semantic(KclErrorDetails::new(
1039 format!("{} requires at least one face or edge.", kind.label()),
1040 vec![args.source_range],
1041 )));
1042 }
1043
1044 let precision = resolve_precision(precision, args)?;
1045 let datums = resolve_datums(datums, args, kind.label())?;
1046 let frame_plane = resolve_gdt_frame_plane(frame_plane, exec_state, args).await?;
1047 let symbol = kind.symbol();
1048
1049 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1050 for face in &faces {
1051 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1052 create_feature_control_annotation(
1053 face_id,
1054 symbol,
1055 &tolerance,
1056 &datums,
1057 precision,
1058 frame_position.as_ref(),
1059 frame_plane.id,
1060 leader_scale.as_ref(),
1061 font_size.as_ref(),
1062 exec_state,
1063 args,
1064 &mut annotations,
1065 )
1066 .await?;
1067 }
1068 for edge in &edges {
1069 let edge_id = edge.get_engine_id(exec_state, args)?;
1070 create_feature_control_annotation(
1071 edge_id,
1072 symbol,
1073 &tolerance,
1074 &datums,
1075 precision,
1076 frame_position.as_ref(),
1077 frame_plane.id,
1078 leader_scale.as_ref(),
1079 font_size.as_ref(),
1080 exec_state,
1081 args,
1082 &mut annotations,
1083 )
1084 .await?;
1085 }
1086
1087 Ok(annotations)
1088}
1089
1090#[allow(clippy::too_many_arguments)]
1091async fn create_feature_control_annotation(
1092 entity_id: uuid::Uuid,
1093 symbol: MbdSymbol,
1094 tolerance: &TyF64,
1095 datums: &[char],
1096 precision: u32,
1097 frame_position: Option<&[TyF64; 2]>,
1098 frame_plane_id: uuid::Uuid,
1099 leader_scale: Option<&TyF64>,
1100 font_size: Option<&TyF64>,
1101 exec_state: &mut ExecState,
1102 args: &Args,
1103 annotations: &mut Vec<GdtAnnotation>,
1104) -> Result<(), KclError> {
1105 let meta = vec![Metadata::from(args.source_range)];
1106 let annotation_id = exec_state.next_uuid();
1107 let display_units = exec_state.length_unit();
1108 let control_frame = gdt_control_frame(symbol, tolerance.to_length_units(display_units), datums);
1109 let feature_control = AnnotationFeatureControl::builder()
1110 .entity_id(entity_id)
1111 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1112 .leader_type(AnnotationLineEnd::Dot)
1113 .control_frame(control_frame)
1114 .plane_id(frame_plane_id)
1115 .offset(if let Some(offset) = frame_position {
1116 KPoint2d {
1117 x: offset[0].to_mm(),
1118 y: offset[1].to_mm(),
1119 }
1120 } else {
1121 KPoint2d { x: 100.0, y: 100.0 }
1122 })
1123 .precision(precision)
1124 .font_scale(gdt_font_scale(font_size, args)?)
1125 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1126 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1127 .build();
1128 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1129 exec_state
1130 .batch_modeling_cmd(
1131 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1132 ModelingCmd::from(
1133 mcmd::NewAnnotation::builder()
1134 .options(options)
1135 .clobber(false)
1136 .annotation_type(AnnotationType::T3D)
1137 .build(),
1138 ),
1139 )
1140 .await?;
1141 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1142 annotations.push(GdtAnnotation {
1143 id: annotation_id,
1144 meta,
1145 });
1146 Ok(())
1147}
1148
1149fn gdt_control_frame(symbol: MbdSymbol, tolerance: f64, datums: &[char]) -> AnnotationMbdControlFrame {
1150 match datums {
1151 [] => AnnotationMbdControlFrame::builder()
1152 .symbol(symbol)
1153 .tolerance(tolerance)
1154 .build(),
1155 [primary] => AnnotationMbdControlFrame::builder()
1156 .symbol(symbol)
1157 .tolerance(tolerance)
1158 .primary_datum(*primary)
1159 .build(),
1160 [primary, secondary] => AnnotationMbdControlFrame::builder()
1161 .symbol(symbol)
1162 .tolerance(tolerance)
1163 .primary_datum(*primary)
1164 .secondary_datum(*secondary)
1165 .build(),
1166 [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1167 .symbol(symbol)
1168 .tolerance(tolerance)
1169 .primary_datum(*primary)
1170 .secondary_datum(*secondary)
1171 .tertiary_datum(*tertiary)
1172 .build(),
1173 _ => unreachable!("resolve_datums rejects more than three datums"),
1174 }
1175}
1176
1177#[allow(clippy::too_many_arguments)]
1178async fn create_annotation(
1179 entity_id: uuid::Uuid,
1180 annotation: &str,
1181 frame_position: Option<&[TyF64; 2]>,
1182 frame_plane_id: uuid::Uuid,
1183 leader_scale: Option<&TyF64>,
1184 font_size: Option<&TyF64>,
1185 exec_state: &mut ExecState,
1186 args: &Args,
1187 annotations: &mut Vec<GdtAnnotation>,
1188) -> Result<(), KclError> {
1189 let meta = vec![Metadata::from(args.source_range)];
1190 let annotation_id = exec_state.next_uuid();
1191 let feature_control = AnnotationFeatureControl::builder()
1192 .entity_id(entity_id)
1193 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1194 .leader_type(AnnotationLineEnd::Dot)
1195 .prefix(annotation.to_owned())
1196 .plane_id(frame_plane_id)
1197 .offset(if let Some(offset) = frame_position {
1198 KPoint2d {
1199 x: offset[0].to_mm(),
1200 y: offset[1].to_mm(),
1201 }
1202 } else {
1203 KPoint2d { x: 100.0, y: 100.0 }
1204 })
1205 .precision(0)
1206 .font_scale(gdt_font_scale(font_size, args)?)
1207 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1208 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1209 .build();
1210 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1211 exec_state
1212 .batch_modeling_cmd(
1213 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1214 ModelingCmd::from(
1215 mcmd::NewAnnotation::builder()
1216 .options(options)
1217 .clobber(false)
1218 .annotation_type(AnnotationType::T3D)
1219 .build(),
1220 ),
1221 )
1222 .await?;
1223 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1224 annotations.push(GdtAnnotation {
1225 id: annotation_id,
1226 meta,
1227 });
1228 Ok(())
1229}
1230
1231fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1232 let datums = datums.unwrap_or_default();
1233 if datums.len() > 3 {
1234 return Err(KclError::new_semantic(KclErrorDetails::new(
1235 format!("{annotation_name} datums must include at most three names."),
1236 vec![args.source_range],
1237 )));
1238 }
1239
1240 let mut resolved = Vec::with_capacity(datums.len());
1241 for datum in &datums {
1242 let mut chars = datum.chars();
1243 let Some(name) = chars.next() else {
1244 return Err(KclError::new_semantic(KclErrorDetails::new(
1245 format!("{annotation_name} datum names must be a single character."),
1246 vec![args.source_range],
1247 )));
1248 };
1249 if chars.next().is_some() {
1250 return Err(KclError::new_semantic(KclErrorDetails::new(
1251 format!("{annotation_name} datum names must be a single character."),
1252 vec![args.source_range],
1253 )));
1254 }
1255 resolved.push(name);
1256 }
1257
1258 Ok(resolved)
1259}
1260
1261async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1264 let plane_ast = plane_ast("XY", args.source_range);
1265 let metadata = Metadata::from(args.source_range);
1266 let plane_value = args
1267 .ctx
1268 .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
1269 .await?;
1270 let plane_value = match plane_value.control {
1271 ControlFlowKind::Continue => plane_value.into_value(),
1272 ControlFlowKind::Exit => {
1273 let message = "Early return inside plane value is currently not supported".to_owned();
1274 debug_assert!(false, "{}", &message);
1275 return Err(KclError::new_internal(KclErrorDetails::new(
1276 message,
1277 vec![args.source_range],
1278 )));
1279 }
1280 };
1281 Ok(plane_value
1282 .as_plane()
1283 .ok_or_else(|| {
1284 KclError::new_internal(KclErrorDetails::new(
1285 "Expected XY plane to be defined".to_owned(),
1286 vec![args.source_range],
1287 ))
1288 })?
1289 .clone())
1290}
1291
1292fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1294 ast::Node::new(
1295 ast::Expr::Name(Box::new(ast::Node::new(
1296 ast::Name {
1297 name: ast::Identifier::new(plane_name),
1298 path: Vec::new(),
1299 abs_path: false,
1302 digest: None,
1303 },
1304 range.start(),
1305 range.end(),
1306 range.module_id(),
1307 ))),
1308 range.start(),
1309 range.end(),
1310 range.module_id(),
1311 )
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use super::*;
1317 use crate::ExecutorContext;
1318 use crate::execution::Artifact;
1319 use crate::execution::ExecutorSettings;
1320 use crate::execution::MockConfig;
1321 use crate::execution::parse_execute;
1322
1323 const GDT_DISTANCE_KCL_TEMPLATE: &str = r#"
1324@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1325
1326sketch001 = sketch(on = XY) {
1327 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1328 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1329 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1330 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1331 coincident([line1.end, line2.start])
1332 coincident([line2.end, line3.start])
1333 coincident([line3.end, line4.start])
1334 coincident([line4.end, line1.start])
1335 parallel([line2, line4])
1336 parallel([line3, line1])
1337 perpendicular([line1, line2])
1338 horizontal(line3)
1339}
1340
1341region001 = region(point = [5mm, 5mm], sketch = sketch001)
1342extrude001 = extrude(region001, length = 10mm)
1343gdt::distance(
1344 edges = [
1345 getCommonEdge(faces = [
1346 region001.tags.line4,
1347 region001.tags.line1
1348 ])
1349 ],
1350 tolerance = __TOLERANCE__,
1351 framePosition = __FRAME_POSITION__,
1352 fontSize = 2in,
1353)
1354"#;
1355
1356 const GDT_FLATNESS_KCL_TEMPLATE: &str = r#"
1357@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1358
1359sketch001 = sketch(on = XY) {
1360 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1361 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1362 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1363 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1364 coincident([line1.end, line2.start])
1365 coincident([line2.end, line3.start])
1366 coincident([line3.end, line4.start])
1367 coincident([line4.end, line1.start])
1368 parallel([line2, line4])
1369 parallel([line3, line1])
1370 perpendicular([line1, line2])
1371 horizontal(line3)
1372}
1373
1374region001 = region(point = [5mm, 5mm], sketch = sketch001)
1375extrude001 = extrude(region001, length = 10mm, tagEnd = $capEnd001)
1376gdt::flatness(
1377 faces = [capEnd001],
1378 tolerance = __TOLERANCE__,
1379 framePosition = __FRAME_POSITION__,
1380 framePlane = XZ,
1381 fontSize = 2in,
1382)
1383"#;
1384
1385 fn gdt_distance_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1386 GDT_DISTANCE_KCL_TEMPLATE
1387 .replace("__UNIT__", unit)
1388 .replace("__TOLERANCE__", tolerance)
1389 .replace("__FRAME_POSITION__", frame_position)
1390 }
1391
1392 fn gdt_flatness_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1393 GDT_FLATNESS_KCL_TEMPLATE
1394 .replace("__UNIT__", unit)
1395 .replace("__TOLERANCE__", tolerance)
1396 .replace("__FRAME_POSITION__", frame_position)
1397 }
1398
1399 async fn gdt_commands(code: &str) -> Vec<ModelingCmd> {
1400 let result = parse_execute(code).await.unwrap();
1401 result
1402 .root_module_artifact_commands()
1403 .iter()
1404 .map(|artifact_command| artifact_command.command.clone())
1405 .collect()
1406 }
1407
1408 fn annotation_options(command: &ModelingCmd) -> Result<&AnnotationOptions, KclError> {
1409 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1410 return Err(KclError::new_internal(KclErrorDetails::new(
1411 format!("expected new_annotation command, got {command:?}"),
1412 vec![SourceRange::default()],
1413 )));
1414 };
1415 Ok(&new_annotation.options)
1416 }
1417
1418 fn feature_control(command: &ModelingCmd) -> Result<&AnnotationFeatureControl, KclError> {
1419 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1420 return Err(KclError::new_internal(KclErrorDetails::new(
1421 format!("expected new_annotation command, got {command:?}"),
1422 vec![SourceRange::default()],
1423 )));
1424 };
1425 new_annotation.options.feature_control.as_ref().ok_or_else(|| {
1426 KclError::new_internal(KclErrorDetails::new(
1427 "expected new_annotation command to have a feature_control".to_owned(),
1428 vec![SourceRange::default()],
1429 ))
1430 })
1431 }
1432
1433 fn find_control_frame_with_symbol(
1434 commands: &[ModelingCmd],
1435 symbol: MbdSymbol,
1436 ) -> Result<&AnnotationMbdControlFrame, KclError> {
1437 for command in commands {
1438 if let Ok(feature_control) = feature_control(command)
1439 && let Some(control_frame) = feature_control.control_frame.as_ref()
1440 && control_frame.symbol == symbol
1441 {
1442 return Ok(control_frame);
1443 }
1444 }
1445
1446 Err(KclError::new_internal(KclErrorDetails::new(
1447 format!("expected commands to contain a {symbol:?} control frame"),
1448 vec![SourceRange::default()],
1449 )))
1450 }
1451
1452 #[track_caller]
1453 fn assert_close(actual: f64, expected: f64) {
1454 assert!((actual - expected).abs() < 1e-6, "expected {expected}, got {actual}");
1455 }
1456
1457 fn new_annotation_command_index(commands: &[ModelingCmd]) -> Result<usize, KclError> {
1458 commands
1459 .iter()
1460 .position(|command| matches!(command, ModelingCmd::NewAnnotation(_)))
1461 .ok_or_else(|| {
1462 KclError::new_internal(KclErrorDetails::new(
1463 "expected commands to contain a new_annotation command".to_owned(),
1464 vec![SourceRange::default()],
1465 ))
1466 })
1467 }
1468
1469 #[test]
1470 fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1471 let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1472 assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1473
1474 let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1475 assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1476
1477 let inch_in_mm = 25.4;
1478 let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1479 assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1480 }
1481
1482 const GDT_FLATNESS_LEADER_KCL_TEMPLATE: &str = r#"
1483@settings(defaultLengthUnit = mm, kclVersion = 2)
1484
1485blockProfile = sketch(on = XY) {
1486 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1487 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1488 edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1489 edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1490 coincident([edge1.end, edge2.start])
1491 coincident([edge2.end, edge3.start])
1492 coincident([edge3.end, edge4.start])
1493 coincident([edge4.end, edge1.start])
1494 parallel([edge2, edge4])
1495 parallel([edge3, edge1])
1496 perpendicular([edge1, edge2])
1497 horizontal(edge3)
1498}
1499
1500region001 = region(point = [5mm, 5mm], sketch = blockProfile)
1501extrude001 = extrude(region001, length = 10mm, tagEnd = $top)
1502gdt::flatness(
1503 faces = [top],
1504 tolerance = 0.1mm,
1505 framePosition = [10mm, 0mm],
1506 framePlane = XZ,
1507 fontSize = __FONT_SIZE__
1508 __LEADER_SCALE__
1509)
1510"#;
1511
1512 fn gdt_flatness_leader_kcl(font_size: &str, leader_scale: Option<&str>) -> String {
1513 GDT_FLATNESS_LEADER_KCL_TEMPLATE
1514 .replace("__FONT_SIZE__", font_size)
1515 .replace(
1516 "__LEADER_SCALE__",
1517 leader_scale
1518 .map(|scale| format!(",\n leaderScale = {scale}"))
1519 .unwrap_or_default()
1520 .as_str(),
1521 )
1522 }
1523
1524 async fn gdt_flatness_feature_control(
1525 font_size: &str,
1526 leader_scale: Option<&str>,
1527 ) -> Result<AnnotationFeatureControl, KclError> {
1528 let code = gdt_flatness_leader_kcl(font_size, leader_scale);
1529 let commands = gdt_commands(&code).await;
1530 let annotation_index = new_annotation_command_index(&commands)?;
1531 Ok(feature_control(&commands[annotation_index])?.clone())
1532 }
1533
1534 #[tokio::test(flavor = "multi_thread")]
1535 async fn gdt_dot_leader_scale_is_normalized_against_font_scale() -> Result<(), KclError> {
1536 let tiny = gdt_flatness_feature_control("1mm", None).await?;
1537 let large = gdt_flatness_feature_control("100mm", None).await?;
1538
1539 assert_close(f64::from(tiny.font_scale), gdt_font_scale_for_height_mm(1.0).into());
1540 assert_close(f64::from(large.font_scale), gdt_font_scale_for_height_mm(100.0).into());
1541 assert_close(f64::from(tiny.leader_scale), 50.0);
1542 assert_close(f64::from(large.leader_scale), 0.5);
1543
1544 assert_close(
1545 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1546 f64::from(gdt_dot_leader_normal_size()),
1547 );
1548 assert_close(
1549 f64::from(large.font_scale) * f64::from(large.leader_scale),
1550 f64::from(gdt_dot_leader_normal_size()),
1551 );
1552 Ok(())
1553 }
1554
1555 #[tokio::test(flavor = "multi_thread")]
1556 async fn explicit_gdt_dot_leader_scale_multiplies_normal_size() -> Result<(), KclError> {
1557 let tiny = gdt_flatness_feature_control("1mm", Some("2")).await?;
1558 let large = gdt_flatness_feature_control("100mm", Some("2")).await?;
1559
1560 let expected_scaled_dot_size = f64::from(gdt_dot_leader_normal_size()) * 2.0;
1561 assert_close(
1562 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1563 expected_scaled_dot_size,
1564 );
1565 assert_close(
1566 f64::from(large.font_scale) * f64::from(large.leader_scale),
1567 expected_scaled_dot_size,
1568 );
1569 Ok(())
1570 }
1571
1572 #[tokio::test(flavor = "multi_thread")]
1573 async fn gdt_flatness_uses_scene_units_for_control_frame_tolerance() -> Result<(), KclError> {
1574 let cases = [
1575 ("in", "0.1in", "[10, -10]", 0.1, 254.0, -254.0),
1576 ("cm", "10mm", "[1, -1]", 1.0, 10.0, -10.0),
1577 ];
1578
1579 for (default_unit, tolerance, frame_position, expected_tolerance, expected_x, expected_y) in cases {
1580 let code = gdt_flatness_kcl(default_unit, tolerance, frame_position);
1581 let commands = gdt_commands(&code).await;
1582 let annotation_index = new_annotation_command_index(&commands)?;
1583 let feature_control = feature_control(&commands[annotation_index])?;
1584 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1585 KclError::new_internal(KclErrorDetails::new(
1586 "expected feature_control to have a control_frame".to_owned(),
1587 vec![SourceRange::default()],
1588 ))
1589 })?;
1590
1591 assert_close(control_frame.tolerance, expected_tolerance);
1592 assert_close(feature_control.offset.x, expected_x);
1593 assert_close(feature_control.offset.y, expected_y);
1594 assert_close(
1595 f64::from(feature_control.font_scale),
1596 gdt_font_scale_for_height_mm(50.8).into(),
1597 );
1598 }
1599 Ok(())
1600 }
1601
1602 #[tokio::test(flavor = "multi_thread")]
1603 async fn gdt_distance_sets_units() -> Result<(), KclError> {
1604 let cases = [
1605 (
1606 "in",
1607 "2.54mm",
1608 "[10, -10]",
1609 kcmc::units::UnitLength::Inches,
1610 0.1,
1611 254.0,
1612 -254.0,
1613 ),
1614 (
1615 "cm",
1616 "10mm",
1617 "[1, -1]",
1618 kcmc::units::UnitLength::Centimeters,
1619 1.0,
1620 10.0,
1621 -10.0,
1622 ),
1623 (
1624 "mm",
1625 "2.54mm",
1626 "[10, -10]",
1627 kcmc::units::UnitLength::Millimeters,
1628 2.54,
1629 10.0,
1630 -10.0,
1631 ),
1632 ];
1633
1634 for (default_unit, tolerance, frame_position, scene_unit, expected_tolerance, expected_x, expected_y) in cases {
1635 let code = gdt_distance_kcl(default_unit, tolerance, frame_position);
1636 let commands = gdt_commands(&code).await;
1637 let annotation_index = new_annotation_command_index(&commands)?;
1638 let options = annotation_options(&commands[annotation_index])?;
1639
1640 assert_eq!(options.units, Some(scene_unit));
1641
1642 let dimension = options
1643 .dimension
1644 .as_ref()
1645 .expect("expected new_annotation command to have a dimension");
1646 assert_close(dimension.dimension.tolerance, expected_tolerance);
1647 assert_close(dimension.offset.x, expected_x);
1648 assert_close(dimension.offset.y, expected_y);
1649 assert_close(
1650 f64::from(dimension.font_scale),
1651 gdt_font_scale_for_height_mm(50.8).into(),
1652 );
1653 }
1654 Ok(())
1655 }
1656
1657 const GDT_DATUM_KCL: &str = r#"
1658blockProfile = sketch(on = XY) {
1659 edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
1660 edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
1661 edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
1662 edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
1663 coincident([edge1.end, edge2.start])
1664 coincident([edge2.end, edge3.start])
1665 coincident([edge3.end, edge4.start])
1666 coincident([edge4.end, edge1.start])
1667 horizontal(edge1)
1668 vertical(edge2)
1669 horizontal(edge3)
1670 vertical(edge4)
1671}
1672
1673block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
1674
1675gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
1676"#;
1677
1678 async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
1679 let settings = ExecutorSettings {
1680 skip_artifact_graph,
1681 ..Default::default()
1682 };
1683 let ctx = ExecutorContext::new_mock(Some(settings)).await;
1684 let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
1685 let mock_config = MockConfig {
1686 use_prev_memory: false,
1687 ..Default::default()
1688 };
1689 let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
1690 ctx.close().await;
1691
1692 outcome
1693 .artifact_graph
1694 .values()
1695 .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
1696 .count()
1697 }
1698
1699 #[tokio::test(flavor = "multi_thread")]
1700 async fn gdt_annotations_do_not_follow_runtime_artifact_graph_setting() {
1701 assert_eq!(gdt_artifact_count(false).await, 1);
1702 assert_eq!(gdt_artifact_count(true).await, 1);
1703 }
1704
1705 const GDT_ANGULARITY_FACE_KCL: &str = r#"
1706@settings(defaultLengthUnit = mm, kclVersion = 2)
1707
1708basicAngle = 30deg
1709thickness = 3.5mm
1710flangeLength = 24mm
1711bendStartX = 5mm
1712legLength = 30mm
1713legRun = legLength * cos(basicAngle)
1714legRise = legLength * sin(basicAngle)
1715normalRun = thickness * sin(basicAngle)
1716normalRise = thickness * cos(basicAngle)
1717annotationFont = 2mm
1718
1719stampedProfile = sketch(on = XY) {
1720 datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
1721 flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
1722 innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
1723 controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
1724 tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
1725 outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
1726 outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
1727 coincident([datumFace.end, flangeEnd.start])
1728 coincident([flangeEnd.end, innerFlange.start])
1729 coincident([innerFlange.end, controlledSurface.start])
1730 coincident([controlledSurface.end, tabEnd.start])
1731 coincident([tabEnd.end, outerSurface.start])
1732 coincident([outerSurface.end, outsideBend.start])
1733 coincident([outsideBend.end, datumFace.start])
1734 coincident([datumFace.start, ORIGIN])
1735 horizontal(datumFace)
1736 horizontal(innerFlange)
1737 vertical(flangeEnd)
1738 distance([datumFace.start, datumFace.end]) == flangeLength
1739 distance([flangeEnd.start, flangeEnd.end]) == thickness
1740 distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
1741 distance([controlledSurface.start, controlledSurface.end]) == legLength
1742 distance([tabEnd.start, tabEnd.end]) == thickness
1743 distance([outerSurface.start, outerSurface.end]) == legLength
1744 parallel([controlledSurface, outerSurface])
1745 perpendicular([controlledSurface, tabEnd])
1746 angle([datumFace, controlledSurface]) == basicAngle
1747}
1748
1749stampedPart = extrude(region(point = [12mm, 2mm], sketch = stampedProfile), length = 0.8mm)
1750
1751gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
1752gdt::angularity(faces = [stampedPart.sketch.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
1753"#;
1754
1755 const GDT_ANGULARITY_EDGE_KCL: &str = r#"
1756@settings(defaultLengthUnit = mm, kclVersion = 2)
1757
1758basicAngle = 30deg
1759thickness = 3.5mm
1760flangeLength = 24mm
1761bendStartX = 5mm
1762legLength = 30mm
1763legRun = legLength * cos(basicAngle)
1764legRise = legLength * sin(basicAngle)
1765normalRun = thickness * sin(basicAngle)
1766normalRise = thickness * cos(basicAngle)
1767annotationFont = 2mm
1768
1769stampedProfile = sketch(on = XY) {
1770 datumFace = line(start = [var 0mm, var 0mm], end = [var 24mm, var 0mm])
1771 flangeEnd = line(start = [var 24mm, var 0mm], end = [var 24mm, var 3.5mm])
1772 innerFlange = line(start = [var 24mm, var 3.5mm], end = [var 5mm, var 3.5mm])
1773 controlledSurface = line(start = [var 5mm, var 3.5mm], end = [var 30.98mm, var 18.5mm])
1774 tabEnd = line(start = [var 30.98mm, var 18.5mm], end = [var 29.23mm, var 21.53mm])
1775 outerSurface = line(start = [var 29.23mm, var 21.53mm], end = [var 3.25mm, var 6.53mm])
1776 outsideBend = line(start = [var 3.25mm, var 6.53mm], end = [var 0mm, var 0mm])
1777 coincident([datumFace.end, flangeEnd.start])
1778 coincident([flangeEnd.end, innerFlange.start])
1779 coincident([innerFlange.end, controlledSurface.start])
1780 coincident([controlledSurface.end, tabEnd.start])
1781 coincident([tabEnd.end, outerSurface.start])
1782 coincident([outerSurface.end, outsideBend.start])
1783 coincident([outsideBend.end, datumFace.start])
1784 coincident([datumFace.start, ORIGIN])
1785 horizontal(datumFace)
1786 horizontal(innerFlange)
1787 vertical(flangeEnd)
1788 distance([datumFace.start, datumFace.end]) == flangeLength
1789 distance([flangeEnd.start, flangeEnd.end]) == thickness
1790 distance([innerFlange.start, innerFlange.end]) == flangeLength - bendStartX
1791 distance([controlledSurface.start, controlledSurface.end]) == legLength
1792 distance([tabEnd.start, tabEnd.end]) == thickness
1793 distance([outerSurface.start, outerSurface.end]) == legLength
1794 parallel([controlledSurface, outerSurface])
1795 perpendicular([controlledSurface, tabEnd])
1796 angle([datumFace, controlledSurface]) == basicAngle
1797}
1798
1799stampedRegion = region(point = [12mm, 2mm], sketch = stampedProfile)
1800hide(stampedProfile)
1801stampedPart = extrude(stampedRegion, length = 0.8mm)
1802
1803gdt::datum(face = stampedPart.sketch.tags.datumFace, name = "A", framePosition = [6mm, -4mm], framePlane = XY, fontSize = annotationFont)
1804gdt::angularity(edges = [stampedRegion.tags.controlledSurface], tolerance = 0.1mm, datums = ["A"], framePosition = [-12mm, 11mm], framePlane = XZ, fontSize = annotationFont)
1805"#;
1806
1807 #[tokio::test(flavor = "multi_thread")]
1808 async fn gdt_angularity_uses_angularity_symbol_with_datums() -> Result<(), KclError> {
1809 let cases = [
1810 ("angled face", GDT_ANGULARITY_FACE_KCL, 0.1),
1811 ("angled edge", GDT_ANGULARITY_EDGE_KCL, 0.1),
1812 ];
1813
1814 for (label, code, expected_tolerance) in cases {
1815 let commands = gdt_commands(code).await;
1816 let control_frame = find_control_frame_with_symbol(&commands, MbdSymbol::Angularity)?;
1817
1818 assert_close(control_frame.tolerance, expected_tolerance);
1819 assert_eq!(control_frame.primary_datum, Some('A'), "case: {label}");
1820 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
1821 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
1822 }
1823 Ok(())
1824 }
1825
1826 const GDT_CIRCULARITY_EDGE_KCL: &str = r#"
1832@settings(defaultLengthUnit = mm, kclVersion = 2)
1833
1834cylinderSketch = sketch(on = XY) {
1835 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1836}
1837
1838cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
1839hide(cylinderSketch)
1840cylinder = extrude(cylinderRegion, length = 10mm)
1841gdt::circularity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm)
1842"#;
1843
1844 const GDT_CIRCULARITY_WALL_KCL: &str = r#"
1845@settings(defaultLengthUnit = mm, kclVersion = 2)
1846
1847cylinderSketch = sketch(on = XY) {
1848 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1849}
1850
1851cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
1852gdt::circularity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [12mm, 8mm], framePlane = XZ)
1853"#;
1854
1855 const GDT_CIRCULARITY_COMMON_EDGE_KCL: &str = r#"
1856@settings(defaultLengthUnit = mm, kclVersion = 2)
1857
1858cylinderSketch = sketch(on = XY) {
1859 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1860}
1861
1862cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
1863topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
1864gdt::circularity(edges = [topEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
1865"#;
1866
1867 #[tokio::test(flavor = "multi_thread")]
1868 async fn gdt_circularity_uses_roundness_symbol_without_datums() -> Result<(), KclError> {
1869 let cases = [
1870 ("circular edge", GDT_CIRCULARITY_EDGE_KCL, 0.05),
1871 ("cylinder wall", GDT_CIRCULARITY_WALL_KCL, 0.02),
1872 ("common edge", GDT_CIRCULARITY_COMMON_EDGE_KCL, 0.05),
1873 ];
1874
1875 for (label, code, expected_tolerance) in cases {
1876 let commands = gdt_commands(code).await;
1877 let annotation_index = new_annotation_command_index(&commands)?;
1878 let feature_control = feature_control(&commands[annotation_index])?;
1879 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1880 KclError::new_internal(KclErrorDetails::new(
1881 format!("expected {label} feature_control to have a control_frame"),
1882 vec![SourceRange::default()],
1883 ))
1884 })?;
1885
1886 assert_eq!(control_frame.symbol, MbdSymbol::Roundness, "case: {label}");
1887 assert_close(control_frame.tolerance, expected_tolerance);
1888 assert!(control_frame.primary_datum.is_none(), "case: {label}");
1890 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
1891 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
1892 }
1893 Ok(())
1894 }
1895
1896 const GDT_CYLINDRICITY_WALL_KCL: &str = r#"
1902@settings(defaultLengthUnit = mm, kclVersion = 2)
1903
1904cylinderSketch = sketch(on = XY) {
1905 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1906}
1907
1908cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
1909gdt::cylindricity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [-12mm, 8mm], framePlane = XZ)
1910"#;
1911
1912 const GDT_CYLINDRICITY_EDGE_KCL: &str = r#"
1913@settings(defaultLengthUnit = mm, kclVersion = 2)
1914
1915cylinderSketch = sketch(on = XY) {
1916 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1917}
1918
1919cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
1920hide(cylinderSketch)
1921cylinder = extrude(cylinderRegion, length = 10mm)
1922gdt::cylindricity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm, framePosition = [-12mm, 8mm])
1923"#;
1924
1925 const GDT_CYLINDRICITY_COMMON_EDGE_KCL: &str = r#"
1926@settings(defaultLengthUnit = mm, kclVersion = 2)
1927
1928cylinderSketch = sketch(on = XY) {
1929 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1930}
1931
1932cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
1933topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
1934gdt::cylindricity(edges = [topEdge], tolerance = 0.05mm, framePosition = [-12mm, 8mm], framePlane = XZ)
1935"#;
1936
1937 #[tokio::test(flavor = "multi_thread")]
1938 async fn gdt_cylindricity_uses_cylindricity_symbol_without_datums() -> Result<(), KclError> {
1939 let cases = [
1940 ("cylinder wall", GDT_CYLINDRICITY_WALL_KCL, 0.02),
1941 ("circular edge", GDT_CYLINDRICITY_EDGE_KCL, 0.05),
1942 ("common edge", GDT_CYLINDRICITY_COMMON_EDGE_KCL, 0.05),
1943 ];
1944
1945 for (label, code, expected_tolerance) in cases {
1946 let commands = gdt_commands(code).await;
1947 let annotation_index = new_annotation_command_index(&commands)?;
1948 let feature_control = feature_control(&commands[annotation_index])?;
1949 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1950 KclError::new_internal(KclErrorDetails::new(
1951 format!("expected {label} feature_control to have a control_frame"),
1952 vec![SourceRange::default()],
1953 ))
1954 })?;
1955
1956 assert_eq!(control_frame.symbol, MbdSymbol::Cylindricity, "case: {label}");
1957 assert_close(control_frame.tolerance, expected_tolerance);
1958 assert!(control_frame.primary_datum.is_none(), "case: {label}");
1960 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
1961 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
1962 }
1963 Ok(())
1964 }
1965}