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