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