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