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;
47
48const GDT_FONT_SCALE_1_HEIGHT_MM: f64 = 8.0;
52
53fn gdt_font_scale(font_size: Option<&TyF64>, args: &Args) -> Result<f32, KclError> {
54 let requested_height_mm = font_size.map(TyF64::to_mm).unwrap_or(DEFAULT_GDT_FONT_SIZE_MM);
55 if requested_height_mm <= 0.0 {
56 return Err(KclError::new_semantic(KclErrorDetails::new(
57 "fontSize must be greater than 0.".to_owned(),
58 vec![args.source_range],
59 )));
60 }
61 Ok(gdt_font_scale_for_height_mm(requested_height_mm))
62}
63
64fn gdt_font_scale_for_height_mm(requested_height_mm: f64) -> f32 {
65 (requested_height_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32
66}
67
68#[derive(Debug, Clone)]
69enum DistanceEntity {
70 Face(Box<Face>),
71 TaggedFace(Box<TagIdentifier>),
72 Edge(EdgeReference),
73}
74
75#[derive(Debug, Clone, Copy)]
76struct DistanceEndpoint {
77 entity_id: uuid::Uuid,
78 entity_pos: KPoint2d<f64>,
79}
80
81fn add_gdt_annotation_artifact(exec_state: &mut ExecState, args: &Args, annotation_id: uuid::Uuid) {
82 if args.ctx.settings.skip_artifact_graph {
83 return;
84 }
85
86 exec_state.add_artifact(Artifact::GdtAnnotation(GdtAnnotationArtifact {
87 id: ArtifactId::new(annotation_id),
88 code_ref: CodeRef::placeholder(args.source_range),
89 }));
90}
91
92impl DistanceEntity {
93 async fn to_endpoint(&self, exec_state: &mut ExecState, args: &Args) -> Result<DistanceEndpoint, KclError> {
94 match self {
95 DistanceEntity::Face(face) => Ok(DistanceEndpoint {
96 entity_id: face.id,
97 entity_pos: KPoint2d { x: 0.5, y: 0.5 },
98 }),
99 DistanceEntity::TaggedFace(face) => Ok(DistanceEndpoint {
100 entity_id: args.get_adjacent_face_to_tag(exec_state, face, false).await?,
101 entity_pos: KPoint2d { x: 0.5, y: 0.5 },
102 }),
103 DistanceEntity::Edge(edge) => Ok(DistanceEndpoint {
104 entity_id: edge.get_engine_id(exec_state, args)?,
105 entity_pos: KPoint2d { x: 0.5, y: 0.0 },
106 }),
107 }
108 }
109}
110
111impl<'a> FromKclValue<'a> for DistanceEntity {
112 fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
113 match arg {
114 KclValue::Face { value } => Some(Self::Face(value.to_owned())),
115 KclValue::Uuid { value, .. } => Some(Self::Edge(EdgeReference::Uuid(*value))),
116 KclValue::TagIdentifier(value) => Some(Self::TaggedFace(value.to_owned())),
117 _ => None,
118 }
119 }
120}
121
122fn distance_entity_type() -> RuntimeType {
123 RuntimeType::Union(vec![
124 RuntimeType::face(),
125 RuntimeType::tagged_face(),
126 RuntimeType::edge(),
127 ])
128}
129
130pub async fn datum(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
131 let face: TagIdentifier = args.get_kw_arg("face", &RuntimeType::tagged_face(), exec_state)?;
132 let name: String = args.get_kw_arg("name", &RuntimeType::string(), exec_state)?;
133 let frame_position: Option<[TyF64; 2]> =
134 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
135 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
136 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
137 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
138
139 let annotation = inner_datum(
140 face,
141 name,
142 frame_position,
143 frame_plane,
144 leader_scale,
145 font_size,
146 exec_state,
147 &args,
148 )
149 .await?;
150 Ok(KclValue::GdtAnnotation {
151 value: Box::new(annotation),
152 })
153}
154
155#[allow(clippy::too_many_arguments)]
156async fn inner_datum(
157 face: TagIdentifier,
158 name: String,
159 frame_position: Option<[TyF64; 2]>,
160 frame_plane: Option<Plane>,
161 leader_scale: Option<TyF64>,
162 font_size: Option<TyF64>,
163 exec_state: &mut ExecState,
164 args: &Args,
165) -> Result<GdtAnnotation, KclError> {
166 const DATUM_LENGTH_ERROR: &str = "Datum name must be a single character.";
167 if name.len() > 1 {
168 return Err(KclError::new_semantic(KclErrorDetails::new(
169 DATUM_LENGTH_ERROR.to_owned(),
170 vec![args.source_range],
171 )));
172 }
173 let name_char = name.chars().next().ok_or_else(|| {
174 KclError::new_semantic(KclErrorDetails::new(
175 DATUM_LENGTH_ERROR.to_owned(),
176 vec![args.source_range],
177 ))
178 })?;
179 let mut frame_plane = if let Some(plane) = frame_plane {
180 plane
181 } else {
182 xy_plane(exec_state, args).await?
184 };
185 ensure_sketch_plane_in_engine(
186 &mut frame_plane,
187 exec_state,
188 &args.ctx,
189 args.source_range,
190 args.node_path.clone(),
191 )
192 .await?;
193 let face_id = args.get_adjacent_face_to_tag(exec_state, &face, false).await?;
194 let meta = vec![Metadata::from(args.source_range)];
195 let annotation_id = exec_state.next_uuid();
196 let feature_control = AnnotationFeatureControl::builder()
197 .entity_id(face_id)
198 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
200 .leader_type(AnnotationLineEnd::Dot)
201 .defined_datum(name_char)
202 .plane_id(frame_plane.id)
203 .offset(if let Some(offset) = &frame_position {
204 KPoint2d {
205 x: offset[0].to_mm(),
206 y: offset[1].to_mm(),
207 }
208 } else {
209 KPoint2d { x: 100.0, y: 100.0 }
210 })
211 .precision(0)
212 .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
213 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
214 .leader_scale(leader_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0))
215 .build();
216 exec_state
217 .batch_modeling_cmd(
218 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
219 ModelingCmd::from(
220 mcmd::NewAnnotation::builder()
221 .options(AnnotationOptions::builder().feature_control(feature_control).build())
222 .clobber(false)
223 .annotation_type(AnnotationType::T3D)
224 .build(),
225 ),
226 )
227 .await?;
228 add_gdt_annotation_artifact(exec_state, args, annotation_id);
229 Ok(GdtAnnotation {
230 id: annotation_id,
231 meta,
232 })
233}
234
235pub async fn flatness(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
236 let faces: Vec<TagIdentifier> = args.get_kw_arg(
237 "faces",
238 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
239 exec_state,
240 )?;
241 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
242 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
243 let frame_position: Option<[TyF64; 2]> =
244 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
245 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
246 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
247 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
248
249 let annotations = inner_flatness(
250 faces,
251 tolerance,
252 precision,
253 frame_position,
254 frame_plane,
255 leader_scale,
256 font_size,
257 exec_state,
258 &args,
259 )
260 .await?;
261 Ok(annotations.into())
262}
263
264pub async fn profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
265 let edges: Vec<EdgeReference> = args.get_kw_arg(
266 "edges",
267 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
268 exec_state,
269 )?;
270 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
271 "datums",
272 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
273 exec_state,
274 )?;
275 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
276 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
277 let frame_position: Option<[TyF64; 2]> =
278 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
279 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
280 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
281 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
282
283 let annotations = inner_profile(
284 edges,
285 datums,
286 tolerance,
287 precision,
288 frame_position,
289 frame_plane,
290 leader_scale,
291 font_size,
292 exec_state,
293 &args,
294 )
295 .await?;
296 Ok(annotations.into())
297}
298
299pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
300 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
301 "faces",
302 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
303 exec_state,
304 )?;
305 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
306 "edges",
307 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
308 exec_state,
309 )?;
310 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
311 "datums",
312 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
313 exec_state,
314 )?;
315 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
316 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
317 let frame_position: Option<[TyF64; 2]> =
318 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
319 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
320 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
321 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
322
323 let annotations = inner_position(
324 faces.unwrap_or_default(),
325 edges.unwrap_or_default(),
326 tolerance,
327 datums,
328 precision,
329 frame_position,
330 frame_plane,
331 leader_scale,
332 font_size,
333 exec_state,
334 &args,
335 )
336 .await?;
337 Ok(annotations.into())
338}
339
340pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
341 let from: Option<DistanceEntity> = args.get_kw_arg_opt("from", &distance_entity_type(), exec_state)?;
342 let to: Option<DistanceEntity> = args.get_kw_arg_opt("to", &distance_entity_type(), exec_state)?;
343 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
344 "edges",
345 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
346 exec_state,
347 )?;
348 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
349 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
350 let frame_position: Option<[TyF64; 2]> =
351 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
352 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
353 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
354 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
355
356 let annotations = inner_distance(
357 from,
358 to,
359 edges.unwrap_or_default(),
360 tolerance,
361 precision,
362 frame_position,
363 frame_plane,
364 leader_scale,
365 font_size,
366 exec_state,
367 &args,
368 )
369 .await?;
370 Ok(annotations.into())
371}
372
373pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
374 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
375 "faces",
376 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
377 exec_state,
378 )?;
379 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
380 "edges",
381 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
382 exec_state,
383 )?;
384 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
385 "datums",
386 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
387 exec_state,
388 )?;
389 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
390 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
391 let frame_position: Option<[TyF64; 2]> =
392 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
393 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
394 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
395 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
396
397 let annotations = inner_perpendicularity(
398 faces.unwrap_or_default(),
399 edges.unwrap_or_default(),
400 datums,
401 tolerance,
402 precision,
403 frame_position,
404 frame_plane,
405 leader_scale,
406 font_size,
407 exec_state,
408 &args,
409 )
410 .await?;
411 Ok(annotations.into())
412}
413
414pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
415 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
416 "faces",
417 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
418 exec_state,
419 )?;
420 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
421 "edges",
422 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
423 exec_state,
424 )?;
425 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
426 "datums",
427 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
428 exec_state,
429 )?;
430 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
431 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
432 let frame_position: Option<[TyF64; 2]> =
433 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
434 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
435 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
436 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
437
438 let annotations = inner_parallelism(
439 faces.unwrap_or_default(),
440 edges.unwrap_or_default(),
441 datums,
442 tolerance,
443 precision,
444 frame_position,
445 frame_plane,
446 leader_scale,
447 font_size,
448 exec_state,
449 &args,
450 )
451 .await?;
452 Ok(annotations.into())
453}
454
455pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
456 let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
457 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
458 "faces",
459 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
460 exec_state,
461 )?;
462 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
463 "edges",
464 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
465 exec_state,
466 )?;
467 let frame_position: Option<[TyF64; 2]> =
468 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
469 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
470 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
471 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
472
473 let annotations = inner_annotation(
474 annotation,
475 faces.unwrap_or_default(),
476 edges.unwrap_or_default(),
477 frame_position,
478 frame_plane,
479 leader_scale,
480 font_size,
481 exec_state,
482 &args,
483 )
484 .await?;
485 Ok(annotations.into())
486}
487
488#[allow(clippy::too_many_arguments)]
489async fn inner_perpendicularity(
490 faces: Vec<TagIdentifier>,
491 edges: Vec<EdgeReference>,
492 datums: Option<Vec<String>>,
493 tolerance: TyF64,
494 precision: Option<TyF64>,
495 frame_position: Option<[TyF64; 2]>,
496 frame_plane: Option<Plane>,
497 leader_scale: Option<TyF64>,
498 font_size: Option<TyF64>,
499 exec_state: &mut ExecState,
500 args: &Args,
501) -> Result<Vec<GdtAnnotation>, KclError> {
502 if faces.is_empty() && edges.is_empty() {
503 return Err(KclError::new_semantic(KclErrorDetails::new(
504 "Perpendicularity requires at least one face or edge.".to_owned(),
505 vec![args.source_range],
506 )));
507 }
508
509 let precision = resolve_precision(precision, args)?;
510 let datums = resolve_datums(datums, args, "Perpendicularity")?;
511 let mut frame_plane = if let Some(plane) = frame_plane {
512 plane
513 } else {
514 xy_plane(exec_state, args).await?
515 };
516 ensure_sketch_plane_in_engine(
517 &mut frame_plane,
518 exec_state,
519 &args.ctx,
520 args.source_range,
521 args.node_path.clone(),
522 )
523 .await?;
524
525 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
526 for face in &faces {
527 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
528 create_feature_control_annotation(
529 face_id,
530 MbdSymbol::Perpendicularity,
531 &tolerance,
532 &datums,
533 precision,
534 frame_position.as_ref(),
535 frame_plane.id,
536 leader_scale.as_ref(),
537 font_size.as_ref(),
538 exec_state,
539 args,
540 &mut annotations,
541 )
542 .await?;
543 }
544 for edge in &edges {
545 let edge_id = edge.get_engine_id(exec_state, args)?;
546 create_feature_control_annotation(
547 edge_id,
548 MbdSymbol::Perpendicularity,
549 &tolerance,
550 &datums,
551 precision,
552 frame_position.as_ref(),
553 frame_plane.id,
554 leader_scale.as_ref(),
555 font_size.as_ref(),
556 exec_state,
557 args,
558 &mut annotations,
559 )
560 .await?;
561 }
562
563 Ok(annotations)
564}
565
566#[allow(clippy::too_many_arguments)]
567async fn inner_parallelism(
568 faces: Vec<TagIdentifier>,
569 edges: Vec<EdgeReference>,
570 datums: Option<Vec<String>>,
571 tolerance: TyF64,
572 precision: Option<TyF64>,
573 frame_position: Option<[TyF64; 2]>,
574 frame_plane: Option<Plane>,
575 leader_scale: Option<TyF64>,
576 font_size: Option<TyF64>,
577 exec_state: &mut ExecState,
578 args: &Args,
579) -> Result<Vec<GdtAnnotation>, KclError> {
580 if faces.is_empty() && edges.is_empty() {
581 return Err(KclError::new_semantic(KclErrorDetails::new(
582 "Parallelism requires at least one face or edge.".to_owned(),
583 vec![args.source_range],
584 )));
585 }
586
587 let precision = resolve_precision(precision, args)?;
588 let datums = resolve_datums(datums, args, "Parallelism")?;
589 let mut frame_plane = if let Some(plane) = frame_plane {
590 plane
591 } else {
592 xy_plane(exec_state, args).await?
593 };
594 ensure_sketch_plane_in_engine(
595 &mut frame_plane,
596 exec_state,
597 &args.ctx,
598 args.source_range,
599 args.node_path.clone(),
600 )
601 .await?;
602
603 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
604 for face in &faces {
605 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
606 create_feature_control_annotation(
607 face_id,
608 MbdSymbol::Parallelism,
609 &tolerance,
610 &datums,
611 precision,
612 frame_position.as_ref(),
613 frame_plane.id,
614 leader_scale.as_ref(),
615 font_size.as_ref(),
616 exec_state,
617 args,
618 &mut annotations,
619 )
620 .await?;
621 }
622 for edge in &edges {
623 let edge_id = edge.get_engine_id(exec_state, args)?;
624 create_feature_control_annotation(
625 edge_id,
626 MbdSymbol::Parallelism,
627 &tolerance,
628 &datums,
629 precision,
630 frame_position.as_ref(),
631 frame_plane.id,
632 leader_scale.as_ref(),
633 font_size.as_ref(),
634 exec_state,
635 args,
636 &mut annotations,
637 )
638 .await?;
639 }
640
641 Ok(annotations)
642}
643
644#[allow(clippy::too_many_arguments)]
645async fn inner_annotation(
646 annotation: String,
647 faces: Vec<TagIdentifier>,
648 edges: Vec<EdgeReference>,
649 frame_position: Option<[TyF64; 2]>,
650 frame_plane: Option<Plane>,
651 leader_scale: Option<TyF64>,
652 font_size: Option<TyF64>,
653 exec_state: &mut ExecState,
654 args: &Args,
655) -> Result<Vec<GdtAnnotation>, KclError> {
656 if annotation.is_empty() {
657 return Err(KclError::new_semantic(KclErrorDetails::new(
658 "Annotation text must not be empty.".to_owned(),
659 vec![args.source_range],
660 )));
661 }
662 if faces.is_empty() && edges.is_empty() {
663 return Err(KclError::new_semantic(KclErrorDetails::new(
664 "Annotation requires at least one face or edge.".to_owned(),
665 vec![args.source_range],
666 )));
667 }
668
669 let mut frame_plane = if let Some(plane) = frame_plane {
670 plane
671 } else {
672 xy_plane(exec_state, args).await?
673 };
674 ensure_sketch_plane_in_engine(
675 &mut frame_plane,
676 exec_state,
677 &args.ctx,
678 args.source_range,
679 args.node_path.clone(),
680 )
681 .await?;
682
683 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
684 for face in &faces {
685 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
686 create_annotation(
687 face_id,
688 &annotation,
689 frame_position.as_ref(),
690 frame_plane.id,
691 leader_scale.as_ref(),
692 font_size.as_ref(),
693 exec_state,
694 args,
695 &mut annotations,
696 )
697 .await?;
698 }
699 for edge in &edges {
700 let edge_id = edge.get_engine_id(exec_state, args)?;
701 create_annotation(
702 edge_id,
703 &annotation,
704 frame_position.as_ref(),
705 frame_plane.id,
706 leader_scale.as_ref(),
707 font_size.as_ref(),
708 exec_state,
709 args,
710 &mut annotations,
711 )
712 .await?;
713 }
714
715 Ok(annotations)
716}
717
718#[allow(clippy::too_many_arguments)]
719async fn inner_distance(
720 from: Option<DistanceEntity>,
721 to: Option<DistanceEntity>,
722 edges: Vec<EdgeReference>,
723 tolerance: TyF64,
724 precision: Option<TyF64>,
725 frame_position: Option<[TyF64; 2]>,
726 frame_plane: Option<Plane>,
727 leader_scale: Option<TyF64>,
728 font_size: Option<TyF64>,
729 exec_state: &mut ExecState,
730 args: &Args,
731) -> Result<Vec<GdtAnnotation>, KclError> {
732 let precision = resolve_precision(precision, args)?;
733 let mut frame_plane = if let Some(plane) = frame_plane {
734 plane
735 } else {
736 xy_plane(exec_state, args).await?
737 };
738 ensure_sketch_plane_in_engine(
739 &mut frame_plane,
740 exec_state,
741 &args.ctx,
742 args.source_range,
743 args.node_path.clone(),
744 )
745 .await?;
746
747 if from.is_some() || to.is_some() {
748 if !edges.is_empty() {
749 return Err(KclError::new_semantic(KclErrorDetails::new(
750 "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
751 vec![args.source_range],
752 )));
753 }
754
755 let (Some(from), Some(to)) = (from, to) else {
756 return Err(KclError::new_semantic(KclErrorDetails::new(
757 "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
758 vec![args.source_range],
759 )));
760 };
761
762 let from = from.to_endpoint(exec_state, args).await?;
763 let to = to.to_endpoint(exec_state, args).await?;
764 let mut annotations = Vec::with_capacity(1);
765 create_basic_distance_annotation(
766 from,
767 to,
768 &tolerance,
769 precision,
770 frame_position.as_ref(),
771 frame_plane.id,
772 leader_scale.as_ref(),
773 font_size.as_ref(),
774 exec_state,
775 args,
776 &mut annotations,
777 )
778 .await?;
779 return Ok(annotations);
780 }
781
782 if edges.is_empty() {
783 return Err(KclError::new_semantic(KclErrorDetails::new(
784 "Distance requires either `edges` or both `from` and `to`.".to_owned(),
785 vec![args.source_range],
786 )));
787 }
788
789 let mut annotations = Vec::with_capacity(edges.len());
790 for edge in &edges {
791 let edge_id = edge.get_engine_id(exec_state, args)?;
792 create_basic_distance_annotation(
793 DistanceEndpoint {
794 entity_id: edge_id,
795 entity_pos: KPoint2d { x: 0.0, y: 0.0 },
796 },
797 DistanceEndpoint {
798 entity_id: edge_id,
799 entity_pos: KPoint2d { x: 1.0, y: 0.0 },
800 },
801 &tolerance,
802 precision,
803 frame_position.as_ref(),
804 frame_plane.id,
805 leader_scale.as_ref(),
806 font_size.as_ref(),
807 exec_state,
808 args,
809 &mut annotations,
810 )
811 .await?;
812 }
813 Ok(annotations)
814}
815
816#[allow(clippy::too_many_arguments)]
817async fn inner_profile(
818 edges: Vec<EdgeReference>,
819 datums: Option<Vec<String>>,
820 tolerance: TyF64,
821 precision: Option<TyF64>,
822 frame_position: Option<[TyF64; 2]>,
823 frame_plane: Option<Plane>,
824 leader_scale: Option<TyF64>,
825 font_size: Option<TyF64>,
826 exec_state: &mut ExecState,
827 args: &Args,
828) -> Result<Vec<GdtAnnotation>, KclError> {
829 let precision = resolve_precision(precision, args)?;
830 let datums = resolve_datums(datums, args, "Profile")?;
831 let mut frame_plane = if let Some(plane) = frame_plane {
832 plane
833 } else {
834 xy_plane(exec_state, args).await?
835 };
836 ensure_sketch_plane_in_engine(
837 &mut frame_plane,
838 exec_state,
839 &args.ctx,
840 args.source_range,
841 args.node_path.clone(),
842 )
843 .await?;
844
845 let mut annotations = Vec::with_capacity(edges.len());
846 for edge in &edges {
847 let edge_id = edge.get_engine_id(exec_state, args)?;
848 create_feature_control_annotation(
849 edge_id,
850 MbdSymbol::ProfileOfLine,
851 &tolerance,
852 &datums,
853 precision,
854 frame_position.as_ref(),
855 frame_plane.id,
856 leader_scale.as_ref(),
857 font_size.as_ref(),
858 exec_state,
859 args,
860 &mut annotations,
861 )
862 .await?;
863 }
864 Ok(annotations)
865}
866
867#[allow(clippy::too_many_arguments)]
868async fn inner_position(
869 faces: Vec<TagIdentifier>,
870 edges: Vec<EdgeReference>,
871 tolerance: TyF64,
872 datums: Option<Vec<String>>,
873 precision: Option<TyF64>,
874 frame_position: Option<[TyF64; 2]>,
875 frame_plane: Option<Plane>,
876 leader_scale: Option<TyF64>,
877 font_size: Option<TyF64>,
878 exec_state: &mut ExecState,
879 args: &Args,
880) -> Result<Vec<GdtAnnotation>, KclError> {
881 if faces.is_empty() && edges.is_empty() {
882 return Err(KclError::new_semantic(KclErrorDetails::new(
883 "Position requires at least one face or edge.".to_owned(),
884 vec![args.source_range],
885 )));
886 }
887
888 let precision = resolve_precision(precision, args)?;
889 let datums = resolve_datums(datums, args, "Position")?;
890 let mut frame_plane = if let Some(plane) = frame_plane {
891 plane
892 } else {
893 xy_plane(exec_state, args).await?
894 };
895 ensure_sketch_plane_in_engine(
896 &mut frame_plane,
897 exec_state,
898 &args.ctx,
899 args.source_range,
900 args.node_path.clone(),
901 )
902 .await?;
903
904 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
905 for face in &faces {
906 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
907 create_feature_control_annotation(
908 face_id,
909 MbdSymbol::Position,
910 &tolerance,
911 &datums,
912 precision,
913 frame_position.as_ref(),
914 frame_plane.id,
915 leader_scale.as_ref(),
916 font_size.as_ref(),
917 exec_state,
918 args,
919 &mut annotations,
920 )
921 .await?;
922 }
923 for edge in &edges {
924 let edge_id = edge.get_engine_id(exec_state, args)?;
925 create_feature_control_annotation(
926 edge_id,
927 MbdSymbol::Position,
928 &tolerance,
929 &datums,
930 precision,
931 frame_position.as_ref(),
932 frame_plane.id,
933 leader_scale.as_ref(),
934 font_size.as_ref(),
935 exec_state,
936 args,
937 &mut annotations,
938 )
939 .await?;
940 }
941 Ok(annotations)
942}
943
944#[allow(clippy::too_many_arguments)]
945async fn inner_flatness(
946 faces: Vec<TagIdentifier>,
947 tolerance: TyF64,
948 precision: Option<TyF64>,
949 frame_position: Option<[TyF64; 2]>,
950 frame_plane: Option<Plane>,
951 leader_scale: Option<TyF64>,
952 font_size: Option<TyF64>,
953 exec_state: &mut ExecState,
954 args: &Args,
955) -> Result<Vec<GdtAnnotation>, KclError> {
956 let precision = resolve_precision(precision, args)?;
957 let mut frame_plane = if let Some(plane) = frame_plane {
958 plane
959 } else {
960 xy_plane(exec_state, args).await?
962 };
963 ensure_sketch_plane_in_engine(
964 &mut frame_plane,
965 exec_state,
966 &args.ctx,
967 args.source_range,
968 args.node_path.clone(),
969 )
970 .await?;
971 let mut annotations = Vec::with_capacity(faces.len());
972 for face in &faces {
973 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
974 let meta = vec![Metadata::from(args.source_range)];
975 let annotation_id = exec_state.next_uuid();
976 let feature_control = AnnotationFeatureControl::builder()
977 .entity_id(face_id)
978 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
980 .leader_type(AnnotationLineEnd::Dot)
981 .control_frame(
982 AnnotationMbdControlFrame::builder()
983 .symbol(MbdSymbol::Flatness)
984 .tolerance(tolerance.to_mm())
985 .build(),
986 )
987 .plane_id(frame_plane.id)
988 .offset(if let Some(offset) = &frame_position {
989 KPoint2d {
990 x: offset[0].to_mm(),
991 y: offset[1].to_mm(),
992 }
993 } else {
994 KPoint2d { x: 100.0, y: 100.0 }
995 })
996 .precision(precision)
997 .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
998 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
999 .leader_scale(leader_scale.as_ref().map(|n| n.n as f32).unwrap_or(1.0))
1000 .build();
1001 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1002 exec_state
1003 .batch_modeling_cmd(
1004 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1005 ModelingCmd::from(
1006 mcmd::NewAnnotation::builder()
1007 .options(options)
1008 .clobber(false)
1009 .annotation_type(AnnotationType::T3D)
1010 .build(),
1011 ),
1012 )
1013 .await?;
1014 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1015 annotations.push(GdtAnnotation {
1016 id: annotation_id,
1017 meta,
1018 });
1019 }
1020 Ok(annotations)
1021}
1022
1023fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
1024 if let Some(precision) = precision {
1025 let rounded = precision.n.round();
1026 if !(0.0..=9.0).contains(&rounded) {
1027 return Err(KclError::new_semantic(KclErrorDetails::new(
1028 "Precision must be between 0 and 9".to_owned(),
1029 vec![args.source_range],
1030 )));
1031 }
1032 Ok(rounded as u32)
1033 } else {
1034 Ok(3)
1035 }
1036}
1037
1038#[allow(clippy::too_many_arguments)]
1039async fn create_basic_distance_annotation(
1040 from: DistanceEndpoint,
1041 to: DistanceEndpoint,
1042 tolerance: &TyF64,
1043 precision: u32,
1044 frame_position: Option<&[TyF64; 2]>,
1045 frame_plane_id: uuid::Uuid,
1046 leader_scale: Option<&TyF64>,
1047 font_size: Option<&TyF64>,
1048 exec_state: &mut ExecState,
1049 args: &Args,
1050 annotations: &mut Vec<GdtAnnotation>,
1051) -> Result<(), KclError> {
1052 let meta = vec![Metadata::from(args.source_range)];
1053 let annotation_id = exec_state.next_uuid();
1054 let dimension = AnnotationBasicDimension::builder()
1055 .from_entity_id(from.entity_id)
1056 .from_entity_pos(from.entity_pos)
1057 .to_entity_id(to.entity_id)
1058 .to_entity_pos(to.entity_pos)
1059 .dimension(
1060 AnnotationMbdBasicDimension::builder()
1061 .tolerance(tolerance.to_mm())
1062 .build(),
1063 )
1064 .plane_id(frame_plane_id)
1065 .offset(if let Some(offset) = frame_position {
1066 KPoint2d {
1067 x: offset[0].to_mm(),
1068 y: offset[1].to_mm(),
1069 }
1070 } else {
1071 KPoint2d { x: 100.0, y: 100.0 }
1072 })
1073 .precision(precision)
1074 .font_scale(gdt_font_scale(font_size, args)?)
1075 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1076 .arrow_scale(leader_scale.map(|n| n.n as f32).unwrap_or(1.0))
1077 .build();
1078 let options = AnnotationOptions::builder().dimension(dimension).build();
1079 exec_state
1080 .batch_modeling_cmd(
1081 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1082 ModelingCmd::from(
1083 mcmd::NewAnnotation::builder()
1084 .options(options)
1085 .clobber(false)
1086 .annotation_type(AnnotationType::T3D)
1087 .build(),
1088 ),
1089 )
1090 .await?;
1091 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1092 annotations.push(GdtAnnotation {
1093 id: annotation_id,
1094 meta,
1095 });
1096 Ok(())
1097}
1098
1099#[allow(clippy::too_many_arguments)]
1100async fn create_feature_control_annotation(
1101 entity_id: uuid::Uuid,
1102 symbol: MbdSymbol,
1103 tolerance: &TyF64,
1104 datums: &[char],
1105 precision: u32,
1106 frame_position: Option<&[TyF64; 2]>,
1107 frame_plane_id: uuid::Uuid,
1108 leader_scale: Option<&TyF64>,
1109 font_size: Option<&TyF64>,
1110 exec_state: &mut ExecState,
1111 args: &Args,
1112 annotations: &mut Vec<GdtAnnotation>,
1113) -> Result<(), KclError> {
1114 let meta = vec![Metadata::from(args.source_range)];
1115 let annotation_id = exec_state.next_uuid();
1116 let control_frame = gdt_control_frame(symbol, tolerance.to_mm(), datums);
1117 let feature_control = AnnotationFeatureControl::builder()
1118 .entity_id(entity_id)
1119 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1120 .leader_type(AnnotationLineEnd::Dot)
1121 .control_frame(control_frame)
1122 .plane_id(frame_plane_id)
1123 .offset(if let Some(offset) = frame_position {
1124 KPoint2d {
1125 x: offset[0].to_mm(),
1126 y: offset[1].to_mm(),
1127 }
1128 } else {
1129 KPoint2d { x: 100.0, y: 100.0 }
1130 })
1131 .precision(precision)
1132 .font_scale(gdt_font_scale(font_size, args)?)
1133 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1134 .leader_scale(leader_scale.map(|n| n.n as f32).unwrap_or(1.0))
1135 .build();
1136 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1137 exec_state
1138 .batch_modeling_cmd(
1139 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1140 ModelingCmd::from(
1141 mcmd::NewAnnotation::builder()
1142 .options(options)
1143 .clobber(false)
1144 .annotation_type(AnnotationType::T3D)
1145 .build(),
1146 ),
1147 )
1148 .await?;
1149 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1150 annotations.push(GdtAnnotation {
1151 id: annotation_id,
1152 meta,
1153 });
1154 Ok(())
1155}
1156
1157#[allow(clippy::too_many_arguments)]
1158async fn create_annotation(
1159 entity_id: uuid::Uuid,
1160 annotation: &str,
1161 frame_position: Option<&[TyF64; 2]>,
1162 frame_plane_id: uuid::Uuid,
1163 leader_scale: Option<&TyF64>,
1164 font_size: Option<&TyF64>,
1165 exec_state: &mut ExecState,
1166 args: &Args,
1167 annotations: &mut Vec<GdtAnnotation>,
1168) -> Result<(), KclError> {
1169 let meta = vec![Metadata::from(args.source_range)];
1170 let annotation_id = exec_state.next_uuid();
1171 let feature_control = AnnotationFeatureControl::builder()
1172 .entity_id(entity_id)
1173 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1174 .leader_type(AnnotationLineEnd::Dot)
1175 .prefix(annotation.to_owned())
1176 .plane_id(frame_plane_id)
1177 .offset(if let Some(offset) = frame_position {
1178 KPoint2d {
1179 x: offset[0].to_mm(),
1180 y: offset[1].to_mm(),
1181 }
1182 } else {
1183 KPoint2d { x: 100.0, y: 100.0 }
1184 })
1185 .precision(0)
1186 .font_scale(gdt_font_scale(font_size, args)?)
1187 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1188 .leader_scale(leader_scale.map(|n| n.n as f32).unwrap_or(1.0))
1189 .build();
1190 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1191 exec_state
1192 .batch_modeling_cmd(
1193 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1194 ModelingCmd::from(
1195 mcmd::NewAnnotation::builder()
1196 .options(options)
1197 .clobber(false)
1198 .annotation_type(AnnotationType::T3D)
1199 .build(),
1200 ),
1201 )
1202 .await?;
1203 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1204 annotations.push(GdtAnnotation {
1205 id: annotation_id,
1206 meta,
1207 });
1208 Ok(())
1209}
1210
1211fn gdt_control_frame(symbol: MbdSymbol, tolerance: f64, datums: &[char]) -> AnnotationMbdControlFrame {
1212 match datums {
1213 [] => AnnotationMbdControlFrame::builder()
1214 .symbol(symbol)
1215 .tolerance(tolerance)
1216 .build(),
1217 [primary] => AnnotationMbdControlFrame::builder()
1218 .symbol(symbol)
1219 .tolerance(tolerance)
1220 .primary_datum(*primary)
1221 .build(),
1222 [primary, secondary] => AnnotationMbdControlFrame::builder()
1223 .symbol(symbol)
1224 .tolerance(tolerance)
1225 .primary_datum(*primary)
1226 .secondary_datum(*secondary)
1227 .build(),
1228 [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1229 .symbol(symbol)
1230 .tolerance(tolerance)
1231 .primary_datum(*primary)
1232 .secondary_datum(*secondary)
1233 .tertiary_datum(*tertiary)
1234 .build(),
1235 _ => unreachable!("resolve_datums rejects more than three datums"),
1236 }
1237}
1238
1239fn resolve_datums(datums: Option<Vec<String>>, args: &Args, annotation_name: &str) -> Result<Vec<char>, KclError> {
1240 let datums = datums.unwrap_or_default();
1241 if datums.len() > 3 {
1242 return Err(KclError::new_semantic(KclErrorDetails::new(
1243 format!("{annotation_name} datums must include at most three names."),
1244 vec![args.source_range],
1245 )));
1246 }
1247
1248 let mut resolved = Vec::with_capacity(datums.len());
1249 for datum in &datums {
1250 let mut chars = datum.chars();
1251 let Some(name) = chars.next() else {
1252 return Err(KclError::new_semantic(KclErrorDetails::new(
1253 format!("{annotation_name} datum names must be a single character."),
1254 vec![args.source_range],
1255 )));
1256 };
1257 if chars.next().is_some() {
1258 return Err(KclError::new_semantic(KclErrorDetails::new(
1259 format!("{annotation_name} datum names must be a single character."),
1260 vec![args.source_range],
1261 )));
1262 }
1263 resolved.push(name);
1264 }
1265
1266 Ok(resolved)
1267}
1268
1269async fn xy_plane(exec_state: &mut ExecState, args: &Args) -> Result<Plane, KclError> {
1272 let plane_ast = plane_ast("XY", args.source_range);
1273 let metadata = Metadata::from(args.source_range);
1274 let plane_value = args
1275 .ctx
1276 .execute_expr(&plane_ast, exec_state, &metadata, &[], StatementKind::Expression)
1277 .await?;
1278 let plane_value = match plane_value.control {
1279 ControlFlowKind::Continue => plane_value.into_value(),
1280 ControlFlowKind::Exit => {
1281 let message = "Early return inside plane value is currently not supported".to_owned();
1282 debug_assert!(false, "{}", &message);
1283 return Err(KclError::new_internal(KclErrorDetails::new(
1284 message,
1285 vec![args.source_range],
1286 )));
1287 }
1288 };
1289 Ok(plane_value
1290 .as_plane()
1291 .ok_or_else(|| {
1292 KclError::new_internal(KclErrorDetails::new(
1293 "Expected XY plane to be defined".to_owned(),
1294 vec![args.source_range],
1295 ))
1296 })?
1297 .clone())
1298}
1299
1300fn plane_ast(plane_name: &str, range: SourceRange) -> ast::Node<ast::Expr> {
1302 ast::Node::new(
1303 ast::Expr::Name(Box::new(ast::Node::new(
1304 ast::Name {
1305 name: ast::Identifier::new(plane_name),
1306 path: Vec::new(),
1307 abs_path: false,
1310 digest: None,
1311 },
1312 range.start(),
1313 range.end(),
1314 range.module_id(),
1315 ))),
1316 range.start(),
1317 range.end(),
1318 range.module_id(),
1319 )
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324 use super::*;
1325 use crate::ExecutorContext;
1326 use crate::execution::Artifact;
1327 use crate::execution::ExecutorSettings;
1328 use crate::execution::MockConfig;
1329
1330 #[test]
1331 fn gdt_font_scale_is_scene_height_divided_by_calibration_height() {
1332 let scale_at_calibrated_height = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM);
1333 assert!((scale_at_calibrated_height - 1.0).abs() < f32::EPSILON);
1334
1335 let double_height_scale = gdt_font_scale_for_height_mm(GDT_FONT_SCALE_1_HEIGHT_MM * 2.0);
1336 assert!((double_height_scale - 2.0).abs() < f32::EPSILON);
1337
1338 let inch_in_mm = 25.4;
1339 let inch_scale = gdt_font_scale_for_height_mm(inch_in_mm);
1340 assert!((inch_scale - (inch_in_mm / GDT_FONT_SCALE_1_HEIGHT_MM) as f32).abs() < f32::EPSILON);
1341 }
1342
1343 const GDT_DATUM_KCL: &str = r#"
1344blockProfile = sketch(on = XY) {
1345 edge1 = line(start = [var 0mm, var 0mm], end = [var 8mm, var 0mm])
1346 edge2 = line(start = [var 8mm, var 0mm], end = [var 8mm, var 5mm])
1347 edge3 = line(start = [var 8mm, var 5mm], end = [var 0mm, var 5mm])
1348 edge4 = line(start = [var 0mm, var 5mm], end = [var 0mm, var 0mm])
1349 coincident([edge1.end, edge2.start])
1350 coincident([edge2.end, edge3.start])
1351 coincident([edge3.end, edge4.start])
1352 coincident([edge4.end, edge1.start])
1353 horizontal(edge1)
1354 vertical(edge2)
1355 horizontal(edge3)
1356 vertical(edge4)
1357}
1358
1359block = extrude(region(point = [4mm, 2mm], sketch = blockProfile), length = 4mm, tagEnd = $top)
1360
1361gdt::datum(face = top, name = "A", framePosition = [10mm, 0mm], framePlane = XZ)
1362"#;
1363
1364 async fn gdt_artifact_count(skip_artifact_graph: bool) -> usize {
1365 let settings = ExecutorSettings {
1366 skip_artifact_graph,
1367 ..Default::default()
1368 };
1369 let ctx = ExecutorContext::new_mock(Some(settings)).await;
1370 let program = crate::Program::parse_no_errs(GDT_DATUM_KCL).unwrap();
1371 let mock_config = MockConfig {
1372 use_prev_memory: false,
1373 ..Default::default()
1374 };
1375 let outcome = ctx.run_mock(&program, &mock_config).await.unwrap();
1376 ctx.close().await;
1377
1378 outcome
1379 .artifact_graph
1380 .values()
1381 .filter(|artifact| matches!(artifact, Artifact::GdtAnnotation(_)))
1382 .count()
1383 }
1384
1385 #[tokio::test(flavor = "multi_thread")]
1386 async fn gdt_annotations_follow_runtime_artifact_graph_setting() {
1387 assert_eq!(gdt_artifact_count(false).await, 1);
1388 assert_eq!(gdt_artifact_count(true).await, 0);
1389 }
1390}