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 profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
593 let edges: Vec<EdgeReference> = args.get_kw_arg(
594 "edges",
595 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
596 exec_state,
597 )?;
598 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
599 "datums",
600 &RuntimeType::Array(Box::new(RuntimeType::string()), 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_profile(
612 edges,
613 datums,
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#[allow(clippy::too_many_arguments)]
628async fn inner_profile(
629 edges: Vec<EdgeReference>,
630 datums: Option<Vec<String>>,
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 let precision = resolve_precision(precision, args)?;
641 let datums = resolve_datums(datums, args, "Profile")?;
642 let mut frame_plane = if let Some(plane) = frame_plane {
643 plane
644 } else {
645 xy_plane(exec_state, args).await?
646 };
647 ensure_sketch_plane_in_engine(
648 &mut frame_plane,
649 exec_state,
650 &args.ctx,
651 args.source_range,
652 args.node_path.clone(),
653 )
654 .await?;
655
656 let mut annotations = Vec::with_capacity(edges.len());
657 for edge in &edges {
658 let edge_id = edge.get_engine_id(exec_state, args)?;
659 create_feature_control_annotation(
660 edge_id,
661 MbdSymbol::ProfileOfLine,
662 &tolerance,
663 &datums,
664 precision,
665 frame_position.as_ref(),
666 frame_plane.id,
667 leader_scale.as_ref(),
668 font_size.as_ref(),
669 exec_state,
670 args,
671 &mut annotations,
672 )
673 .await?;
674 }
675 Ok(annotations)
676}
677
678pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
679 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
680 "faces",
681 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
682 exec_state,
683 )?;
684 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
685 "edges",
686 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
687 exec_state,
688 )?;
689 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
690 "datums",
691 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
692 exec_state,
693 )?;
694 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
695 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
696 let frame_position: Option<[TyF64; 2]> =
697 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
698 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
699 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
700 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
701
702 let annotations = inner_position(
703 faces.unwrap_or_default(),
704 edges.unwrap_or_default(),
705 tolerance,
706 datums,
707 precision,
708 frame_position,
709 frame_plane,
710 leader_scale,
711 font_size,
712 exec_state,
713 &args,
714 )
715 .await?;
716 Ok(annotations.into())
717}
718
719#[allow(clippy::too_many_arguments)]
720async fn inner_position(
721 faces: Vec<TagIdentifier>,
722 edges: Vec<EdgeReference>,
723 tolerance: TyF64,
724 datums: Option<Vec<String>>,
725 precision: Option<TyF64>,
726 frame_position: Option<[TyF64; 2]>,
727 frame_plane: Option<Plane>,
728 leader_scale: Option<TyF64>,
729 font_size: Option<TyF64>,
730 exec_state: &mut ExecState,
731 args: &Args,
732) -> Result<Vec<GdtAnnotation>, KclError> {
733 if faces.is_empty() && edges.is_empty() {
734 return Err(KclError::new_semantic(KclErrorDetails::new(
735 "Position requires at least one face or edge.".to_owned(),
736 vec![args.source_range],
737 )));
738 }
739
740 let precision = resolve_precision(precision, args)?;
741 let datums = resolve_datums(datums, args, "Position")?;
742 let mut frame_plane = if let Some(plane) = frame_plane {
743 plane
744 } else {
745 xy_plane(exec_state, args).await?
746 };
747 ensure_sketch_plane_in_engine(
748 &mut frame_plane,
749 exec_state,
750 &args.ctx,
751 args.source_range,
752 args.node_path.clone(),
753 )
754 .await?;
755
756 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
757 for face in &faces {
758 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
759 create_feature_control_annotation(
760 face_id,
761 MbdSymbol::Position,
762 &tolerance,
763 &datums,
764 precision,
765 frame_position.as_ref(),
766 frame_plane.id,
767 leader_scale.as_ref(),
768 font_size.as_ref(),
769 exec_state,
770 args,
771 &mut annotations,
772 )
773 .await?;
774 }
775 for edge in &edges {
776 let edge_id = edge.get_engine_id(exec_state, args)?;
777 create_feature_control_annotation(
778 edge_id,
779 MbdSymbol::Position,
780 &tolerance,
781 &datums,
782 precision,
783 frame_position.as_ref(),
784 frame_plane.id,
785 leader_scale.as_ref(),
786 font_size.as_ref(),
787 exec_state,
788 args,
789 &mut annotations,
790 )
791 .await?;
792 }
793 Ok(annotations)
794}
795
796pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
797 let from: Option<DistanceEntity> = args.get_kw_arg_opt("from", &distance_entity_type(), exec_state)?;
798 let to: Option<DistanceEntity> = args.get_kw_arg_opt("to", &distance_entity_type(), exec_state)?;
799 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
800 "edges",
801 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
802 exec_state,
803 )?;
804 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
805 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
806 let frame_position: Option<[TyF64; 2]> =
807 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
808 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
809 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
810 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
811
812 let annotations = inner_distance(
813 from,
814 to,
815 edges.unwrap_or_default(),
816 tolerance,
817 precision,
818 frame_position,
819 frame_plane,
820 leader_scale,
821 font_size,
822 exec_state,
823 &args,
824 )
825 .await?;
826 Ok(annotations.into())
827}
828
829#[allow(clippy::too_many_arguments)]
830async fn inner_distance(
831 from: Option<DistanceEntity>,
832 to: Option<DistanceEntity>,
833 edges: Vec<EdgeReference>,
834 tolerance: TyF64,
835 precision: Option<TyF64>,
836 frame_position: Option<[TyF64; 2]>,
837 frame_plane: Option<Plane>,
838 leader_scale: Option<TyF64>,
839 font_size: Option<TyF64>,
840 exec_state: &mut ExecState,
841 args: &Args,
842) -> Result<Vec<GdtAnnotation>, KclError> {
843 let precision = resolve_precision(precision, args)?;
844 let mut frame_plane = if let Some(plane) = frame_plane {
845 plane
846 } else {
847 xy_plane(exec_state, args).await?
848 };
849 ensure_sketch_plane_in_engine(
850 &mut frame_plane,
851 exec_state,
852 &args.ctx,
853 args.source_range,
854 args.node_path.clone(),
855 )
856 .await?;
857
858 if from.is_some() || to.is_some() {
859 if !edges.is_empty() {
860 return Err(KclError::new_semantic(KclErrorDetails::new(
861 "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
862 vec![args.source_range],
863 )));
864 }
865
866 let (Some(from), Some(to)) = (from, to) else {
867 return Err(KclError::new_semantic(KclErrorDetails::new(
868 "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
869 vec![args.source_range],
870 )));
871 };
872
873 let from = from.to_endpoint(exec_state, args).await?;
874 let to = to.to_endpoint(exec_state, args).await?;
875 let mut annotations = Vec::with_capacity(1);
876 create_basic_distance_annotation(
877 from,
878 to,
879 &tolerance,
880 precision,
881 frame_position.as_ref(),
882 frame_plane.id,
883 leader_scale.as_ref(),
884 font_size.as_ref(),
885 exec_state,
886 args,
887 &mut annotations,
888 )
889 .await?;
890 return Ok(annotations);
891 }
892
893 if edges.is_empty() {
894 return Err(KclError::new_semantic(KclErrorDetails::new(
895 "Distance requires either `edges` or both `from` and `to`.".to_owned(),
896 vec![args.source_range],
897 )));
898 }
899
900 let mut annotations = Vec::with_capacity(edges.len());
901 for edge in &edges {
902 let edge_id = edge.get_engine_id(exec_state, args)?;
903 create_basic_distance_annotation(
904 DistanceEndpoint {
905 entity_id: edge_id,
906 entity_pos: KPoint2d { x: 0.0, y: 0.0 },
907 },
908 DistanceEndpoint {
909 entity_id: edge_id,
910 entity_pos: KPoint2d { x: 1.0, y: 0.0 },
911 },
912 &tolerance,
913 precision,
914 frame_position.as_ref(),
915 frame_plane.id,
916 leader_scale.as_ref(),
917 font_size.as_ref(),
918 exec_state,
919 args,
920 &mut annotations,
921 )
922 .await?;
923 }
924 Ok(annotations)
925}
926
927#[allow(clippy::too_many_arguments)]
928async fn create_basic_distance_annotation(
929 from: DistanceEndpoint,
930 to: DistanceEndpoint,
931 tolerance: &TyF64,
932 precision: u32,
933 frame_position: Option<&[TyF64; 2]>,
934 frame_plane_id: uuid::Uuid,
935 leader_scale: Option<&TyF64>,
936 font_size: Option<&TyF64>,
937 exec_state: &mut ExecState,
938 args: &Args,
939 annotations: &mut Vec<GdtAnnotation>,
940) -> Result<(), KclError> {
941 let meta = vec![Metadata::from(args.source_range)];
942 let annotation_id = exec_state.next_uuid();
943 let display_units = exec_state.length_unit();
944 let dimension = AnnotationBasicDimension::builder()
945 .from_entity_id(from.entity_id)
946 .from_entity_pos(from.entity_pos)
947 .to_entity_id(to.entity_id)
948 .to_entity_pos(to.entity_pos)
949 .dimension(
950 AnnotationMbdBasicDimension::builder()
951 .tolerance(tolerance.to_length_units(display_units))
952 .build(),
953 )
954 .plane_id(frame_plane_id)
955 .offset(if let Some(offset) = frame_position {
956 KPoint2d {
957 x: offset[0].to_mm(),
958 y: offset[1].to_mm(),
959 }
960 } else {
961 KPoint2d { x: 100.0, y: 100.0 }
962 })
963 .precision(precision)
964 .font_scale(gdt_font_scale(font_size, args)?)
965 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
966 .arrow_scale(gdt_dimension_leader_scale(leader_scale, args)?)
967 .build();
968 let options = AnnotationOptions::builder()
969 .dimension(dimension)
970 .units(display_units)
971 .build();
972 let annotation_cmd = ModelingCmd::from(
973 mcmd::NewAnnotation::builder()
974 .options(options)
975 .clobber(false)
976 .annotation_type(AnnotationType::T3D)
977 .build(),
978 );
979 let cmd_meta = ModelingCmdMeta::from_args_id(exec_state, args, annotation_id);
980 exec_state.batch_modeling_cmd(cmd_meta, annotation_cmd).await?;
981 add_gdt_annotation_artifact(exec_state, args, annotation_id);
982 annotations.push(GdtAnnotation {
983 id: annotation_id,
984 meta,
985 });
986 Ok(())
987}
988
989pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
990 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
991 "faces",
992 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
993 exec_state,
994 )?;
995 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
996 "edges",
997 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
998 exec_state,
999 )?;
1000 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1001 "datums",
1002 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1003 exec_state,
1004 )?;
1005 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1006 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1007 let frame_position: Option<[TyF64; 2]> =
1008 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1009 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1010 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1011 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1012
1013 let annotations = inner_perpendicularity(
1014 faces.unwrap_or_default(),
1015 edges.unwrap_or_default(),
1016 datums,
1017 tolerance,
1018 precision,
1019 frame_position,
1020 frame_plane,
1021 leader_scale,
1022 font_size,
1023 exec_state,
1024 &args,
1025 )
1026 .await?;
1027 Ok(annotations.into())
1028}
1029
1030#[allow(clippy::too_many_arguments)]
1031async fn inner_perpendicularity(
1032 faces: Vec<TagIdentifier>,
1033 edges: Vec<EdgeReference>,
1034 datums: Option<Vec<String>>,
1035 tolerance: TyF64,
1036 precision: Option<TyF64>,
1037 frame_position: Option<[TyF64; 2]>,
1038 frame_plane: Option<Plane>,
1039 leader_scale: Option<TyF64>,
1040 font_size: Option<TyF64>,
1041 exec_state: &mut ExecState,
1042 args: &Args,
1043) -> Result<Vec<GdtAnnotation>, KclError> {
1044 if faces.is_empty() && edges.is_empty() {
1045 return Err(KclError::new_semantic(KclErrorDetails::new(
1046 "Perpendicularity requires at least one face or edge.".to_owned(),
1047 vec![args.source_range],
1048 )));
1049 }
1050
1051 let precision = resolve_precision(precision, args)?;
1052 let datums = resolve_datums(datums, args, "Perpendicularity")?;
1053 let mut frame_plane = if let Some(plane) = frame_plane {
1054 plane
1055 } else {
1056 xy_plane(exec_state, args).await?
1057 };
1058 ensure_sketch_plane_in_engine(
1059 &mut frame_plane,
1060 exec_state,
1061 &args.ctx,
1062 args.source_range,
1063 args.node_path.clone(),
1064 )
1065 .await?;
1066
1067 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1068 for face in &faces {
1069 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1070 create_feature_control_annotation(
1071 face_id,
1072 MbdSymbol::Perpendicularity,
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 for edge in &edges {
1087 let edge_id = edge.get_engine_id(exec_state, args)?;
1088 create_feature_control_annotation(
1089 edge_id,
1090 MbdSymbol::Perpendicularity,
1091 &tolerance,
1092 &datums,
1093 precision,
1094 frame_position.as_ref(),
1095 frame_plane.id,
1096 leader_scale.as_ref(),
1097 font_size.as_ref(),
1098 exec_state,
1099 args,
1100 &mut annotations,
1101 )
1102 .await?;
1103 }
1104
1105 Ok(annotations)
1106}
1107
1108pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1109 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1110 "faces",
1111 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1112 exec_state,
1113 )?;
1114 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
1115 "edges",
1116 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
1117 exec_state,
1118 )?;
1119 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
1120 "datums",
1121 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
1122 exec_state,
1123 )?;
1124 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
1125 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
1126 let frame_position: Option<[TyF64; 2]> =
1127 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1128 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1129 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1130 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1131
1132 let annotations = inner_parallelism(
1133 faces.unwrap_or_default(),
1134 edges.unwrap_or_default(),
1135 datums,
1136 tolerance,
1137 precision,
1138 frame_position,
1139 frame_plane,
1140 leader_scale,
1141 font_size,
1142 exec_state,
1143 &args,
1144 )
1145 .await?;
1146 Ok(annotations.into())
1147}
1148
1149#[allow(clippy::too_many_arguments)]
1150async fn inner_parallelism(
1151 faces: Vec<TagIdentifier>,
1152 edges: Vec<EdgeReference>,
1153 datums: Option<Vec<String>>,
1154 tolerance: TyF64,
1155 precision: Option<TyF64>,
1156 frame_position: Option<[TyF64; 2]>,
1157 frame_plane: Option<Plane>,
1158 leader_scale: Option<TyF64>,
1159 font_size: Option<TyF64>,
1160 exec_state: &mut ExecState,
1161 args: &Args,
1162) -> Result<Vec<GdtAnnotation>, KclError> {
1163 if faces.is_empty() && edges.is_empty() {
1164 return Err(KclError::new_semantic(KclErrorDetails::new(
1165 "Parallelism requires at least one face or edge.".to_owned(),
1166 vec![args.source_range],
1167 )));
1168 }
1169
1170 let precision = resolve_precision(precision, args)?;
1171 let datums = resolve_datums(datums, args, "Parallelism")?;
1172 let mut frame_plane = if let Some(plane) = frame_plane {
1173 plane
1174 } else {
1175 xy_plane(exec_state, args).await?
1176 };
1177 ensure_sketch_plane_in_engine(
1178 &mut frame_plane,
1179 exec_state,
1180 &args.ctx,
1181 args.source_range,
1182 args.node_path.clone(),
1183 )
1184 .await?;
1185
1186 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1187 for face in &faces {
1188 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1189 create_feature_control_annotation(
1190 face_id,
1191 MbdSymbol::Parallelism,
1192 &tolerance,
1193 &datums,
1194 precision,
1195 frame_position.as_ref(),
1196 frame_plane.id,
1197 leader_scale.as_ref(),
1198 font_size.as_ref(),
1199 exec_state,
1200 args,
1201 &mut annotations,
1202 )
1203 .await?;
1204 }
1205 for edge in &edges {
1206 let edge_id = edge.get_engine_id(exec_state, args)?;
1207 create_feature_control_annotation(
1208 edge_id,
1209 MbdSymbol::Parallelism,
1210 &tolerance,
1211 &datums,
1212 precision,
1213 frame_position.as_ref(),
1214 frame_plane.id,
1215 leader_scale.as_ref(),
1216 font_size.as_ref(),
1217 exec_state,
1218 args,
1219 &mut annotations,
1220 )
1221 .await?;
1222 }
1223
1224 Ok(annotations)
1225}
1226
1227pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1228 let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
1229 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1230 "faces",
1231 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1232 exec_state,
1233 )?;
1234 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
1235 "edges",
1236 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
1237 exec_state,
1238 )?;
1239 let frame_position: Option<[TyF64; 2]> =
1240 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1241 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1242 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1243 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1244
1245 let annotations = inner_annotation(
1246 annotation,
1247 faces.unwrap_or_default(),
1248 edges.unwrap_or_default(),
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_annotation(
1262 annotation: String,
1263 faces: Vec<TagIdentifier>,
1264 edges: Vec<EdgeReference>,
1265 frame_position: Option<[TyF64; 2]>,
1266 frame_plane: Option<Plane>,
1267 leader_scale: Option<TyF64>,
1268 font_size: Option<TyF64>,
1269 exec_state: &mut ExecState,
1270 args: &Args,
1271) -> Result<Vec<GdtAnnotation>, KclError> {
1272 if annotation.is_empty() {
1273 return Err(KclError::new_semantic(KclErrorDetails::new(
1274 "Annotation text must not be empty.".to_owned(),
1275 vec![args.source_range],
1276 )));
1277 }
1278 if faces.is_empty() && edges.is_empty() {
1279 return Err(KclError::new_semantic(KclErrorDetails::new(
1280 "Annotation requires at least one face or edge.".to_owned(),
1281 vec![args.source_range],
1282 )));
1283 }
1284
1285 let mut frame_plane = if let Some(plane) = frame_plane {
1286 plane
1287 } else {
1288 xy_plane(exec_state, args).await?
1289 };
1290 ensure_sketch_plane_in_engine(
1291 &mut frame_plane,
1292 exec_state,
1293 &args.ctx,
1294 args.source_range,
1295 args.node_path.clone(),
1296 )
1297 .await?;
1298
1299 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1300 for face in &faces {
1301 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1302 create_annotation(
1303 face_id,
1304 &annotation,
1305 frame_position.as_ref(),
1306 frame_plane.id,
1307 leader_scale.as_ref(),
1308 font_size.as_ref(),
1309 exec_state,
1310 args,
1311 &mut annotations,
1312 )
1313 .await?;
1314 }
1315 for edge in &edges {
1316 let edge_id = edge.get_engine_id(exec_state, args)?;
1317 create_annotation(
1318 edge_id,
1319 &annotation,
1320 frame_position.as_ref(),
1321 frame_plane.id,
1322 leader_scale.as_ref(),
1323 font_size.as_ref(),
1324 exec_state,
1325 args,
1326 &mut annotations,
1327 )
1328 .await?;
1329 }
1330
1331 Ok(annotations)
1332}
1333
1334fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
1335 if let Some(precision) = precision {
1336 let rounded = precision.n.round();
1337 if !(0.0..=9.0).contains(&rounded) {
1338 return Err(KclError::new_semantic(KclErrorDetails::new(
1339 "Precision must be between 0 and 9".to_owned(),
1340 vec![args.source_range],
1341 )));
1342 }
1343 Ok(rounded as u32)
1344 } else {
1345 Ok(3)
1346 }
1347}
1348
1349#[allow(clippy::too_many_arguments)]
1350async fn create_feature_control_annotation(
1351 entity_id: uuid::Uuid,
1352 symbol: MbdSymbol,
1353 tolerance: &TyF64,
1354 datums: &[char],
1355 precision: u32,
1356 frame_position: Option<&[TyF64; 2]>,
1357 frame_plane_id: uuid::Uuid,
1358 leader_scale: Option<&TyF64>,
1359 font_size: Option<&TyF64>,
1360 exec_state: &mut ExecState,
1361 args: &Args,
1362 annotations: &mut Vec<GdtAnnotation>,
1363) -> Result<(), KclError> {
1364 let meta = vec![Metadata::from(args.source_range)];
1365 let annotation_id = exec_state.next_uuid();
1366 let display_units = exec_state.length_unit();
1367 let control_frame = gdt_control_frame(symbol, tolerance.to_length_units(display_units), datums);
1368 let feature_control = AnnotationFeatureControl::builder()
1369 .entity_id(entity_id)
1370 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1371 .leader_type(AnnotationLineEnd::Dot)
1372 .control_frame(control_frame)
1373 .plane_id(frame_plane_id)
1374 .offset(if let Some(offset) = frame_position {
1375 KPoint2d {
1376 x: offset[0].to_mm(),
1377 y: offset[1].to_mm(),
1378 }
1379 } else {
1380 KPoint2d { x: 100.0, y: 100.0 }
1381 })
1382 .precision(precision)
1383 .font_scale(gdt_font_scale(font_size, args)?)
1384 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1385 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1386 .build();
1387 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1388 exec_state
1389 .batch_modeling_cmd(
1390 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1391 ModelingCmd::from(
1392 mcmd::NewAnnotation::builder()
1393 .options(options)
1394 .clobber(false)
1395 .annotation_type(AnnotationType::T3D)
1396 .build(),
1397 ),
1398 )
1399 .await?;
1400 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1401 annotations.push(GdtAnnotation {
1402 id: annotation_id,
1403 meta,
1404 });
1405 Ok(())
1406}
1407
1408fn gdt_control_frame(symbol: MbdSymbol, tolerance: f64, datums: &[char]) -> AnnotationMbdControlFrame {
1409 match datums {
1410 [] => AnnotationMbdControlFrame::builder()
1411 .symbol(symbol)
1412 .tolerance(tolerance)
1413 .build(),
1414 [primary] => AnnotationMbdControlFrame::builder()
1415 .symbol(symbol)
1416 .tolerance(tolerance)
1417 .primary_datum(*primary)
1418 .build(),
1419 [primary, secondary] => AnnotationMbdControlFrame::builder()
1420 .symbol(symbol)
1421 .tolerance(tolerance)
1422 .primary_datum(*primary)
1423 .secondary_datum(*secondary)
1424 .build(),
1425 [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1426 .symbol(symbol)
1427 .tolerance(tolerance)
1428 .primary_datum(*primary)
1429 .secondary_datum(*secondary)
1430 .tertiary_datum(*tertiary)
1431 .build(),
1432 _ => unreachable!("resolve_datums rejects more than three datums"),
1433 }
1434}
1435
1436#[allow(clippy::too_many_arguments)]
1437async fn create_annotation(
1438 entity_id: uuid::Uuid,
1439 annotation: &str,
1440 frame_position: Option<&[TyF64; 2]>,
1441 frame_plane_id: uuid::Uuid,
1442 leader_scale: Option<&TyF64>,
1443 font_size: Option<&TyF64>,
1444 exec_state: &mut ExecState,
1445 args: &Args,
1446 annotations: &mut Vec<GdtAnnotation>,
1447) -> Result<(), KclError> {
1448 let meta = vec![Metadata::from(args.source_range)];
1449 let annotation_id = exec_state.next_uuid();
1450 let feature_control = AnnotationFeatureControl::builder()
1451 .entity_id(entity_id)
1452 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1453 .leader_type(AnnotationLineEnd::Dot)
1454 .prefix(annotation.to_owned())
1455 .plane_id(frame_plane_id)
1456 .offset(if let Some(offset) = frame_position {
1457 KPoint2d {
1458 x: offset[0].to_mm(),
1459 y: offset[1].to_mm(),
1460 }
1461 } else {
1462 KPoint2d { x: 100.0, y: 100.0 }
1463 })
1464 .precision(0)
1465 .font_scale(gdt_font_scale(font_size, args)?)
1466 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1467 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1468 .build();
1469 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1470 exec_state
1471 .batch_modeling_cmd(
1472 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1473 ModelingCmd::from(
1474 mcmd::NewAnnotation::builder()
1475 .options(options)
1476 .clobber(false)
1477 .annotation_type(AnnotationType::T3D)
1478 .build(),
1479 ),
1480 )
1481 .await?;
1482 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1483 annotations.push(GdtAnnotation {
1484 id: annotation_id,
1485 meta,
1486 });
1487 Ok(())
1488}
1489
1490fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1491 let datums = datums.unwrap_or_default();
1492 if datums.len() > 3 {
1493 return Err(KclError::new_semantic(KclErrorDetails::new(
1494 format!("{annotation_name} datums must include at most three names."),
1495 vec![args.source_range],
1496 )));
1497 }
1498
1499 let mut resolved = Vec::with_capacity(datums.len());
1500 for datum in &datums {
1501 let mut chars = datum.chars();
1502 let Some(name) = chars.next() else {
1503 return Err(KclError::new_semantic(KclErrorDetails::new(
1504 format!("{annotation_name} datum names must be a single character."),
1505 vec![args.source_range],
1506 )));
1507 };
1508 if chars.next().is_some() {
1509 return Err(KclError::new_semantic(KclErrorDetails::new(
1510 format!("{annotation_name} datum names must be a single character."),
1511 vec![args.source_range],
1512 )));
1513 }
1514 resolved.push(name);
1515 }
1516
1517 Ok(resolved)
1518}
1519
1520async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1523 let plane_ast = plane_ast("XY", args.source_range);
1524 let metadata = Metadata::from(args.source_range);
1525 let plane_value = args
1526 .ctx
1527 .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
1528 .await?;
1529 let plane_value = match plane_value.control {
1530 ControlFlowKind::Continue => plane_value.into_value(),
1531 ControlFlowKind::Exit => {
1532 let message = "Early return inside plane value is currently not supported".to_owned();
1533 debug_assert!(false, "{}", &message);
1534 return Err(KclError::new_internal(KclErrorDetails::new(
1535 message,
1536 vec![args.source_range],
1537 )));
1538 }
1539 };
1540 Ok(plane_value
1541 .as_plane()
1542 .ok_or_else(|| {
1543 KclError::new_internal(KclErrorDetails::new(
1544 "Expected XY plane to be defined".to_owned(),
1545 vec![args.source_range],
1546 ))
1547 })?
1548 .clone())
1549}
1550
1551fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1553 ast::Node::new(
1554 ast::Expr::Name(Box::new(ast::Node::new(
1555 ast::Name {
1556 name: ast::Identifier::new(plane_name),
1557 path: Vec::new(),
1558 abs_path: false,
1561 digest: None,
1562 },
1563 range.start(),
1564 range.end(),
1565 range.module_id(),
1566 ))),
1567 range.start(),
1568 range.end(),
1569 range.module_id(),
1570 )
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575 use super::*;
1576 use crate::ExecutorContext;
1577 use crate::execution::Artifact;
1578 use crate::execution::ExecutorSettings;
1579 use crate::execution::MockConfig;
1580 use crate::execution::parse_execute;
1581
1582 const GDT_DISTANCE_KCL_TEMPLATE: &str = r#"
1583@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1584
1585sketch001 = sketch(on = XY) {
1586 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1587 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1588 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1589 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1590 coincident([line1.end, line2.start])
1591 coincident([line2.end, line3.start])
1592 coincident([line3.end, line4.start])
1593 coincident([line4.end, line1.start])
1594 parallel([line2, line4])
1595 parallel([line3, line1])
1596 perpendicular([line1, line2])
1597 horizontal(line3)
1598}
1599
1600region001 = region(point = [5mm, 5mm], sketch = sketch001)
1601extrude001 = extrude(region001, length = 10mm)
1602gdt::distance(
1603 edges = [
1604 getCommonEdge(faces = [
1605 region001.tags.line4,
1606 region001.tags.line1
1607 ])
1608 ],
1609 tolerance = __TOLERANCE__,
1610 framePosition = __FRAME_POSITION__,
1611 fontSize = 2in,
1612)
1613"#;
1614
1615 const GDT_FLATNESS_KCL_TEMPLATE: &str = r#"
1616@settings(defaultLengthUnit = __UNIT__, kclVersion = 2)
1617
1618sketch001 = sketch(on = XY) {
1619 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1620 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1621 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1622 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1623 coincident([line1.end, line2.start])
1624 coincident([line2.end, line3.start])
1625 coincident([line3.end, line4.start])
1626 coincident([line4.end, line1.start])
1627 parallel([line2, line4])
1628 parallel([line3, line1])
1629 perpendicular([line1, line2])
1630 horizontal(line3)
1631}
1632
1633region001 = region(point = [5mm, 5mm], sketch = sketch001)
1634extrude001 = extrude(region001, length = 10mm, tagEnd = $capEnd001)
1635gdt::flatness(
1636 faces = [capEnd001],
1637 tolerance = __TOLERANCE__,
1638 framePosition = __FRAME_POSITION__,
1639 framePlane = XZ,
1640 fontSize = 2in,
1641)
1642"#;
1643
1644 fn gdt_distance_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1645 GDT_DISTANCE_KCL_TEMPLATE
1646 .replace("__UNIT__", unit)
1647 .replace("__TOLERANCE__", tolerance)
1648 .replace("__FRAME_POSITION__", frame_position)
1649 }
1650
1651 fn gdt_flatness_kcl(unit: &str, tolerance: &str, frame_position: &str) -> String {
1652 GDT_FLATNESS_KCL_TEMPLATE
1653 .replace("__UNIT__", unit)
1654 .replace("__TOLERANCE__", tolerance)
1655 .replace("__FRAME_POSITION__", frame_position)
1656 }
1657
1658 async fn gdt_commands(code: &str) -> Vec<ModelingCmd> {
1659 let result = parse_execute(code).await.unwrap();
1660 result
1661 .root_module_artifact_commands()
1662 .iter()
1663 .map(|artifact_command| artifact_command.command.clone())
1664 .collect()
1665 }
1666
1667 fn annotation_options(command: &ModelingCmd) -> Result<&AnnotationOptions, KclError> {
1668 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1669 return Err(KclError::new_internal(KclErrorDetails::new(
1670 format!("expected new_annotation command, got {command:?}"),
1671 vec![SourceRange::default()],
1672 )));
1673 };
1674 Ok(&new_annotation.options)
1675 }
1676
1677 fn feature_control(command: &ModelingCmd) -> Result<&AnnotationFeatureControl, KclError> {
1678 let ModelingCmd::NewAnnotation(new_annotation) = command else {
1679 return Err(KclError::new_internal(KclErrorDetails::new(
1680 format!("expected new_annotation command, got {command:?}"),
1681 vec![SourceRange::default()],
1682 )));
1683 };
1684 new_annotation.options.feature_control.as_ref().ok_or_else(|| {
1685 KclError::new_internal(KclErrorDetails::new(
1686 "expected new_annotation command to have a feature_control".to_owned(),
1687 vec![SourceRange::default()],
1688 ))
1689 })
1690 }
1691
1692 #[track_caller]
1693 fn assert_close(actual: f64, expected: f64) {
1694 assert!((actual - expected).abs() < 1e-6, "expected {expected}, got {actual}");
1695 }
1696
1697 fn new_annotation_command_index(commands: &[ModelingCmd]) -> Result<usize, KclError> {
1698 commands
1699 .iter()
1700 .position(|command| matches!(command, ModelingCmd::NewAnnotation(_)))
1701 .ok_or_else(|| {
1702 KclError::new_internal(KclErrorDetails::new(
1703 "expected commands to contain a new_annotation command".to_owned(),
1704 vec![SourceRange::default()],
1705 ))
1706 })
1707 }
1708
1709 #[test]
1710 fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1711 let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1712 assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1713
1714 let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1715 assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1716
1717 let inch_in_mm = 25.4;
1718 let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1719 assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1720 }
1721
1722 const GDT_FLATNESS_LEADER_KCL_TEMPLATE: &str = r#"
1723@settings(defaultLengthUnit = mm, kclVersion = 2)
1724
1725blockProfile = sketch(on = XY) {
1726 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1727 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1728 edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1729 edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1730 coincident([edge1.end, edge2.start])
1731 coincident([edge2.end, edge3.start])
1732 coincident([edge3.end, edge4.start])
1733 coincident([edge4.end, edge1.start])
1734 parallel([edge2, edge4])
1735 parallel([edge3, edge1])
1736 perpendicular([edge1, edge2])
1737 horizontal(edge3)
1738}
1739
1740region001 = region(point = [5mm, 5mm], sketch = blockProfile)
1741extrude001 = extrude(region001, length = 10mm, tagEnd = $top)
1742gdt::flatness(
1743 faces = [top],
1744 tolerance = 0.1mm,
1745 framePosition = [10mm, 0mm],
1746 framePlane = XZ,
1747 fontSize = __FONT_SIZE__
1748 __LEADER_SCALE__
1749)
1750"#;
1751
1752 fn gdt_flatness_leader_kcl(font_size: &str, leader_scale: Option<&str>) -> String {
1753 GDT_FLATNESS_LEADER_KCL_TEMPLATE
1754 .replace("__FONT_SIZE__", font_size)
1755 .replace(
1756 "__LEADER_SCALE__",
1757 leader_scale
1758 .map(|scale| format!(",\n leaderScale = {scale}"))
1759 .unwrap_or_default()
1760 .as_str(),
1761 )
1762 }
1763
1764 async fn gdt_flatness_feature_control(
1765 font_size: &str,
1766 leader_scale: Option<&str>,
1767 ) -> Result<AnnotationFeatureControl, KclError> {
1768 let code = gdt_flatness_leader_kcl(font_size, leader_scale);
1769 let commands = gdt_commands(&code).await;
1770 let annotation_index = new_annotation_command_index(&commands)?;
1771 Ok(feature_control(&commands[annotation_index])?.clone())
1772 }
1773
1774 #[tokio::test(flavor = "multi_thread")]
1775 async fn gdt_dot_leader_scale_is_normalized_against_font_scale() -> Result<(), KclError> {
1776 let tiny = gdt_flatness_feature_control("1mm", None).await?;
1777 let large = gdt_flatness_feature_control("100mm", None).await?;
1778
1779 assert_close(f64::from(tiny.font_scale), gdt_font_scale_for_height_mm(1.0).into());
1780 assert_close(f64::from(large.font_scale), gdt_font_scale_for_height_mm(100.0).into());
1781 assert_close(f64::from(tiny.leader_scale), 50.0);
1782 assert_close(f64::from(large.leader_scale), 0.5);
1783
1784 assert_close(
1785 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1786 f64::from(gdt_dot_leader_normal_size()),
1787 );
1788 assert_close(
1789 f64::from(large.font_scale) * f64::from(large.leader_scale),
1790 f64::from(gdt_dot_leader_normal_size()),
1791 );
1792 Ok(())
1793 }
1794
1795 #[tokio::test(flavor = "multi_thread")]
1796 async fn explicit_gdt_dot_leader_scale_multiplies_normal_size() -> Result<(), KclError> {
1797 let tiny = gdt_flatness_feature_control("1mm", Some("2")).await?;
1798 let large = gdt_flatness_feature_control("100mm", Some("2")).await?;
1799
1800 let expected_scaled_dot_size = f64::from(gdt_dot_leader_normal_size()) * 2.0;
1801 assert_close(
1802 f64::from(tiny.font_scale) * f64::from(tiny.leader_scale),
1803 expected_scaled_dot_size,
1804 );
1805 assert_close(
1806 f64::from(large.font_scale) * f64::from(large.leader_scale),
1807 expected_scaled_dot_size,
1808 );
1809 Ok(())
1810 }
1811
1812 #[tokio::test(flavor = "multi_thread")]
1813 async fn gdt_flatness_uses_scene_units_for_control_frame_tolerance() -> Result<(), KclError> {
1814 let cases = [
1815 ("in", "0.1in", "[10, -10]", 0.1, 254.0, -254.0),
1816 ("cm", "10mm", "[1, -1]", 1.0, 10.0, -10.0),
1817 ];
1818
1819 for (default_unit, tolerance, frame_position, expected_tolerance, expected_x, expected_y) in cases {
1820 let code = gdt_flatness_kcl(default_unit, tolerance, frame_position);
1821 let commands = gdt_commands(&code).await;
1822 let annotation_index = new_annotation_command_index(&commands)?;
1823 let feature_control = feature_control(&commands[annotation_index])?;
1824 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1825 KclError::new_internal(KclErrorDetails::new(
1826 "expected feature_control to have a control_frame".to_owned(),
1827 vec![SourceRange::default()],
1828 ))
1829 })?;
1830
1831 assert_close(control_frame.tolerance, expected_tolerance);
1832 assert_close(feature_control.offset.x, expected_x);
1833 assert_close(feature_control.offset.y, expected_y);
1834 assert_close(
1835 f64::from(feature_control.font_scale),
1836 gdt_font_scale_for_height_mm(50.8).into(),
1837 );
1838 }
1839 Ok(())
1840 }
1841
1842 #[tokio::test(flavor = "multi_thread")]
1843 async fn gdt_distance_sets_units() -> Result<(), KclError> {
1844 let cases = [
1845 (
1846 "in",
1847 "2.54mm",
1848 "[10, -10]",
1849 kcmc::units::UnitLength::Inches,
1850 0.1,
1851 254.0,
1852 -254.0,
1853 ),
1854 (
1855 "cm",
1856 "10mm",
1857 "[1, -1]",
1858 kcmc::units::UnitLength::Centimeters,
1859 1.0,
1860 10.0,
1861 -10.0,
1862 ),
1863 (
1864 "mm",
1865 "2.54mm",
1866 "[10, -10]",
1867 kcmc::units::UnitLength::Millimeters,
1868 2.54,
1869 10.0,
1870 -10.0,
1871 ),
1872 ];
1873
1874 for (default_unit, tolerance, frame_position, scene_unit, expected_tolerance, expected_x, expected_y) in cases {
1875 let code = gdt_distance_kcl(default_unit, tolerance, frame_position);
1876 let commands = gdt_commands(&code).await;
1877 let annotation_index = new_annotation_command_index(&commands)?;
1878 let options = annotation_options(&commands[annotation_index])?;
1879
1880 assert_eq!(options.units, Some(scene_unit));
1881
1882 let dimension = options
1883 .dimension
1884 .as_ref()
1885 .expect("expected new_annotation command to have a dimension");
1886 assert_close(dimension.dimension.tolerance, expected_tolerance);
1887 assert_close(dimension.offset.x, expected_x);
1888 assert_close(dimension.offset.y, expected_y);
1889 assert_close(
1890 f64::from(dimension.font_scale),
1891 gdt_font_scale_for_height_mm(50.8).into(),
1892 );
1893 }
1894 Ok(())
1895 }
1896
1897 const GDT_DATUM_KCL: &str = r#"
1898blockProfile = sketch(on = XY) {
1899 edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
1900 edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
1901 edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
1902 edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
1903 coincident([edge1.end, edge2.start])
1904 coincident([edge2.end, edge3.start])
1905 coincident([edge3.end, edge4.start])
1906 coincident([edge4.end, edge1.start])
1907 horizontal(edge1)
1908 vertical(edge2)
1909 horizontal(edge3)
1910 vertical(edge4)
1911}
1912
1913block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
1914
1915gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
1916"#;
1917
1918 async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
1919 let settings = ExecutorSettings {
1920 skip_artifact_graph,
1921 ..Default::default()
1922 };
1923 let ctx = ExecutorContext::new_mock(Some(settings)).await;
1924 let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
1925 let mock_config = MockConfig {
1926 use_prev_memory: false,
1927 ..Default::default()
1928 };
1929 let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
1930 ctx.close().await;
1931
1932 outcome
1933 .artifact_graph
1934 .values()
1935 .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
1936 .count()
1937 }
1938
1939 #[tokio::test(flavor = "multi_thread")]
1940 async fn gdt_annotations_do_not_follow_runtime_artifact_graph_setting() {
1941 assert_eq!(gdt_artifact_count(false).await, 1);
1942 assert_eq!(gdt_artifact_count(true).await, 1);
1943 }
1944
1945 const GDT_CIRCULARITY_EDGE_KCL: &str = r#"
1951@settings(defaultLengthUnit = mm, kclVersion = 2)
1952
1953cylinderSketch = sketch(on = XY) {
1954 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1955}
1956
1957cylinderRegion = region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch)
1958hide(cylinderSketch)
1959cylinder = extrude(cylinderRegion, length = 10mm)
1960gdt::circularity(edges = [cylinderRegion.tags.perimeter], tolerance = 0.05mm)
1961"#;
1962
1963 const GDT_CIRCULARITY_WALL_KCL: &str = r#"
1964@settings(defaultLengthUnit = mm, kclVersion = 2)
1965
1966cylinderSketch = sketch(on = XY) {
1967 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1968}
1969
1970cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm)
1971gdt::circularity(faces = [cylinder.sketch.tags.perimeter], tolerance = 0.02mm, framePosition = [12mm, 8mm], framePlane = XZ)
1972"#;
1973
1974 const GDT_CIRCULARITY_COMMON_EDGE_KCL: &str = r#"
1975@settings(defaultLengthUnit = mm, kclVersion = 2)
1976
1977cylinderSketch = sketch(on = XY) {
1978 perimeter = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
1979}
1980
1981cylinder = extrude(region(point = cylinderSketch.perimeter.center, sketch = cylinderSketch), length = 10mm, tagEnd = $top)
1982topEdge = getCommonEdge(faces = [cylinder.sketch.tags.perimeter, top])
1983gdt::circularity(edges = [topEdge], tolerance = 0.05mm, framePosition = [12mm, 8mm], framePlane = XZ)
1984"#;
1985
1986 #[tokio::test(flavor = "multi_thread")]
1987 async fn gdt_circularity_uses_roundness_symbol_without_datums() -> Result<(), KclError> {
1988 let cases = [
1989 ("circular edge", GDT_CIRCULARITY_EDGE_KCL, 0.05),
1990 ("cylinder wall", GDT_CIRCULARITY_WALL_KCL, 0.02),
1991 ("common edge", GDT_CIRCULARITY_COMMON_EDGE_KCL, 0.05),
1992 ];
1993
1994 for (label, code, expected_tolerance) in cases {
1995 let commands = gdt_commands(code).await;
1996 let annotation_index = new_annotation_command_index(&commands)?;
1997 let feature_control = feature_control(&commands[annotation_index])?;
1998 let control_frame = feature_control.control_frame.as_ref().ok_or_else(|| {
1999 KclError::new_internal(KclErrorDetails::new(
2000 format!("expected {label} feature_control to have a control_frame"),
2001 vec![SourceRange::default()],
2002 ))
2003 })?;
2004
2005 assert_eq!(control_frame.symbol, MbdSymbol::Roundness, "case: {label}");
2006 assert_close(control_frame.tolerance, expected_tolerance);
2007 assert!(control_frame.primary_datum.is_none(), "case: {label}");
2009 assert!(control_frame.secondary_datum.is_none(), "case: {label}");
2010 assert!(control_frame.tertiary_datum.is_none(), "case: {label}");
2011 }
2012 Ok(())
2013 }
2014}