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
298#[allow(clippy::too_many_arguments)]
299async fn inner_flatness(
300 faces: Vec<TagIdentifier>,
301 tolerance: TyF64,
302 precision: Option<TyF64>,
303 frame_position: Option<[TyF64; 2]>,
304 frame_plane: Option<Plane>,
305 leader_scale: Option<TyF64>,
306 font_size: Option<TyF64>,
307 exec_state: &mut ExecState,
308 args: &Args,
309) -> Result<Vec<GdtAnnotation>, KclError> {
310 let precision = resolve_precision(precision, args)?;
311 let mut frame_plane = if let Some(plane) = frame_plane {
312 plane
313 } else {
314 xy_plane(exec_state, args).await?
316 };
317 ensure_sketch_plane_in_engine(
318 &mut frame_plane,
319 exec_state,
320 &args.ctx,
321 args.source_range,
322 args.node_path.clone(),
323 )
324 .await?;
325 let mut annotations = Vec::with_capacity(faces.len());
326 let display_units = exec_state.length_unit();
327 for face in &faces {
328 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
329 let meta = vec![Metadata::from(args.source_range)];
330 let annotation_id = exec_state.next_uuid();
331 let feature_control = AnnotationFeatureControl::builder()
332 .entity_id(face_id)
333 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
335 .leader_type(AnnotationLineEnd::Dot)
336 .control_frame(
337 AnnotationMbdControlFrame::builder()
338 .symbol(MbdSymbol::Flatness)
339 .tolerance(tolerance.to_length_units(display_units))
340 .build(),
341 )
342 .plane_id(frame_plane.id)
343 .offset(if let Some(offset) = &frame_position {
344 KPoint2d {
345 x: offset[0].to_mm(),
346 y: offset[1].to_mm(),
347 }
348 } else {
349 KPoint2d { x: 100.0, y: 100.0 }
350 })
351 .precision(precision)
352 .font_scale(gdt_font_scale(font_size.as_ref(), args)?)
353 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
354 .leader_scale(gdt_dot_leader_scale(leader_scale.as_ref(), font_size.as_ref(), args)?)
355 .build();
356 let options = AnnotationOptions::builder().feature_control(feature_control).build();
357 exec_state
358 .batch_modeling_cmd(
359 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
360 ModelingCmd::from(
361 mcmd::NewAnnotation::builder()
362 .options(options)
363 .clobber(false)
364 .annotation_type(AnnotationType::T3D)
365 .build(),
366 ),
367 )
368 .await?;
369 add_gdt_annotation_artifact(exec_state, args, annotation_id);
370 annotations.push(GdtAnnotation {
371 id: annotation_id,
372 meta,
373 });
374 }
375 Ok(annotations)
376}
377
378pub async fn profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
379 let edges: Vec<EdgeReference> = args.get_kw_arg(
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_profile(
398 edges,
399 datums,
400 tolerance,
401 precision,
402 frame_position,
403 frame_plane,
404 leader_scale,
405 font_size,
406 exec_state,
407 &args,
408 )
409 .await?;
410 Ok(annotations.into())
411}
412
413#[allow(clippy::too_many_arguments)]
414async fn inner_profile(
415 edges: Vec<EdgeReference>,
416 datums: Option<Vec<String>>,
417 tolerance: TyF64,
418 precision: Option<TyF64>,
419 frame_position: Option<[TyF64; 2]>,
420 frame_plane: Option<Plane>,
421 leader_scale: Option<TyF64>,
422 font_size: Option<TyF64>,
423 exec_state: &mut ExecState,
424 args: &Args,
425) -> Result<Vec<GdtAnnotation>, KclError> {
426 let precision = resolve_precision(precision, args)?;
427 let datums = resolve_datums(datums, args, "Profile")?;
428 let mut frame_plane = if let Some(plane) = frame_plane {
429 plane
430 } else {
431 xy_plane(exec_state, args).await?
432 };
433 ensure_sketch_plane_in_engine(
434 &mut frame_plane,
435 exec_state,
436 &args.ctx,
437 args.source_range,
438 args.node_path.clone(),
439 )
440 .await?;
441
442 let mut annotations = Vec::with_capacity(edges.len());
443 for edge in &edges {
444 let edge_id = edge.get_engine_id(exec_state, args)?;
445 create_feature_control_annotation(
446 edge_id,
447 MbdSymbol::ProfileOfLine,
448 &tolerance,
449 &datums,
450 precision,
451 frame_position.as_ref(),
452 frame_plane.id,
453 leader_scale.as_ref(),
454 font_size.as_ref(),
455 exec_state,
456 args,
457 &mut annotations,
458 )
459 .await?;
460 }
461 Ok(annotations)
462}
463
464pub async fn position(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
465 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
466 "faces",
467 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
468 exec_state,
469 )?;
470 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
471 "edges",
472 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
473 exec_state,
474 )?;
475 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
476 "datums",
477 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
478 exec_state,
479 )?;
480 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
481 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
482 let frame_position: Option<[TyF64; 2]> =
483 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
484 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
485 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
486 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
487
488 let annotations = inner_position(
489 faces.unwrap_or_default(),
490 edges.unwrap_or_default(),
491 tolerance,
492 datums,
493 precision,
494 frame_position,
495 frame_plane,
496 leader_scale,
497 font_size,
498 exec_state,
499 &args,
500 )
501 .await?;
502 Ok(annotations.into())
503}
504
505#[allow(clippy::too_many_arguments)]
506async fn inner_position(
507 faces: Vec<TagIdentifier>,
508 edges: Vec<EdgeReference>,
509 tolerance: TyF64,
510 datums: Option<Vec<String>>,
511 precision: Option<TyF64>,
512 frame_position: Option<[TyF64; 2]>,
513 frame_plane: Option<Plane>,
514 leader_scale: Option<TyF64>,
515 font_size: Option<TyF64>,
516 exec_state: &mut ExecState,
517 args: &Args,
518) -> Result<Vec<GdtAnnotation>, KclError> {
519 if faces.is_empty() && edges.is_empty() {
520 return Err(KclError::new_semantic(KclErrorDetails::new(
521 "Position requires at least one face or edge.".to_owned(),
522 vec![args.source_range],
523 )));
524 }
525
526 let precision = resolve_precision(precision, args)?;
527 let datums = resolve_datums(datums, args, "Position")?;
528 let mut frame_plane = if let Some(plane) = frame_plane {
529 plane
530 } else {
531 xy_plane(exec_state, args).await?
532 };
533 ensure_sketch_plane_in_engine(
534 &mut frame_plane,
535 exec_state,
536 &args.ctx,
537 args.source_range,
538 args.node_path.clone(),
539 )
540 .await?;
541
542 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
543 for face in &faces {
544 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
545 create_feature_control_annotation(
546 face_id,
547 MbdSymbol::Position,
548 &tolerance,
549 &datums,
550 precision,
551 frame_position.as_ref(),
552 frame_plane.id,
553 leader_scale.as_ref(),
554 font_size.as_ref(),
555 exec_state,
556 args,
557 &mut annotations,
558 )
559 .await?;
560 }
561 for edge in &edges {
562 let edge_id = edge.get_engine_id(exec_state, args)?;
563 create_feature_control_annotation(
564 edge_id,
565 MbdSymbol::Position,
566 &tolerance,
567 &datums,
568 precision,
569 frame_position.as_ref(),
570 frame_plane.id,
571 leader_scale.as_ref(),
572 font_size.as_ref(),
573 exec_state,
574 args,
575 &mut annotations,
576 )
577 .await?;
578 }
579 Ok(annotations)
580}
581
582pub async fn distance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
583 let from: Option<DistanceEntity> = args.get_kw_arg_opt("from", &distance_entity_type(), exec_state)?;
584 let to: Option<DistanceEntity> = args.get_kw_arg_opt("to", &distance_entity_type(), exec_state)?;
585 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
586 "edges",
587 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
588 exec_state,
589 )?;
590 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
591 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
592 let frame_position: Option<[TyF64; 2]> =
593 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
594 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
595 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
596 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
597
598 let annotations = inner_distance(
599 from,
600 to,
601 edges.unwrap_or_default(),
602 tolerance,
603 precision,
604 frame_position,
605 frame_plane,
606 leader_scale,
607 font_size,
608 exec_state,
609 &args,
610 )
611 .await?;
612 Ok(annotations.into())
613}
614
615#[allow(clippy::too_many_arguments)]
616async fn inner_distance(
617 from: Option<DistanceEntity>,
618 to: Option<DistanceEntity>,
619 edges: Vec<EdgeReference>,
620 tolerance: TyF64,
621 precision: Option<TyF64>,
622 frame_position: Option<[TyF64; 2]>,
623 frame_plane: Option<Plane>,
624 leader_scale: Option<TyF64>,
625 font_size: Option<TyF64>,
626 exec_state: &mut ExecState,
627 args: &Args,
628) -> Result<Vec<GdtAnnotation>, KclError> {
629 let precision = resolve_precision(precision, args)?;
630 let mut frame_plane = if let Some(plane) = frame_plane {
631 plane
632 } else {
633 xy_plane(exec_state, args).await?
634 };
635 ensure_sketch_plane_in_engine(
636 &mut frame_plane,
637 exec_state,
638 &args.ctx,
639 args.source_range,
640 args.node_path.clone(),
641 )
642 .await?;
643
644 if from.is_some() || to.is_some() {
645 if !edges.is_empty() {
646 return Err(KclError::new_semantic(KclErrorDetails::new(
647 "Distance cannot combine `from`/`to` with `edges`.".to_owned(),
648 vec![args.source_range],
649 )));
650 }
651
652 let (Some(from), Some(to)) = (from, to) else {
653 return Err(KclError::new_semantic(KclErrorDetails::new(
654 "Distance requires both `from` and `to` when measuring between entities.".to_owned(),
655 vec![args.source_range],
656 )));
657 };
658
659 let from = from.to_endpoint(exec_state, args).await?;
660 let to = to.to_endpoint(exec_state, args).await?;
661 let mut annotations = Vec::with_capacity(1);
662 create_basic_distance_annotation(
663 from,
664 to,
665 &tolerance,
666 precision,
667 frame_position.as_ref(),
668 frame_plane.id,
669 leader_scale.as_ref(),
670 font_size.as_ref(),
671 exec_state,
672 args,
673 &mut annotations,
674 )
675 .await?;
676 return Ok(annotations);
677 }
678
679 if edges.is_empty() {
680 return Err(KclError::new_semantic(KclErrorDetails::new(
681 "Distance requires either `edges` or both `from` and `to`.".to_owned(),
682 vec![args.source_range],
683 )));
684 }
685
686 let mut annotations = Vec::with_capacity(edges.len());
687 for edge in &edges {
688 let edge_id = edge.get_engine_id(exec_state, args)?;
689 create_basic_distance_annotation(
690 DistanceEndpoint {
691 entity_id: edge_id,
692 entity_pos: KPoint2d { x: 0.0, y: 0.0 },
693 },
694 DistanceEndpoint {
695 entity_id: edge_id,
696 entity_pos: KPoint2d { x: 1.0, y: 0.0 },
697 },
698 &tolerance,
699 precision,
700 frame_position.as_ref(),
701 frame_plane.id,
702 leader_scale.as_ref(),
703 font_size.as_ref(),
704 exec_state,
705 args,
706 &mut annotations,
707 )
708 .await?;
709 }
710 Ok(annotations)
711}
712
713#[allow(clippy::too_many_arguments)]
714async fn create_basic_distance_annotation(
715 from: DistanceEndpoint,
716 to: DistanceEndpoint,
717 tolerance: &TyF64,
718 precision: u32,
719 frame_position: Option<&[TyF64; 2]>,
720 frame_plane_id: uuid::Uuid,
721 leader_scale: Option<&TyF64>,
722 font_size: Option<&TyF64>,
723 exec_state: &mut ExecState,
724 args: &Args,
725 annotations: &mut Vec<GdtAnnotation>,
726) -> Result<(), KclError> {
727 let meta = vec![Metadata::from(args.source_range)];
728 let annotation_id = exec_state.next_uuid();
729 let display_units = exec_state.length_unit();
730 let dimension = AnnotationBasicDimension::builder()
731 .from_entity_id(from.entity_id)
732 .from_entity_pos(from.entity_pos)
733 .to_entity_id(to.entity_id)
734 .to_entity_pos(to.entity_pos)
735 .dimension(
736 AnnotationMbdBasicDimension::builder()
737 .tolerance(tolerance.to_length_units(display_units))
738 .build(),
739 )
740 .plane_id(frame_plane_id)
741 .offset(if let Some(offset) = frame_position {
742 KPoint2d {
743 x: offset[0].to_mm(),
744 y: offset[1].to_mm(),
745 }
746 } else {
747 KPoint2d { x: 100.0, y: 100.0 }
748 })
749 .precision(precision)
750 .font_scale(gdt_font_scale(font_size, args)?)
751 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
752 .arrow_scale(gdt_dimension_leader_scale(leader_scale, args)?)
753 .build();
754 let options = AnnotationOptions::builder().dimension(dimension).build();
755 let use_display_units = display_units != kcmc::units::UnitLength::Millimeters;
759 let annotation_cmd = ModelingCmd::from(
760 mcmd::NewAnnotation::builder()
761 .options(options)
762 .clobber(false)
763 .annotation_type(AnnotationType::T3D)
764 .build(),
765 );
766 let cmd_meta = ModelingCmdMeta::from_args_id(exec_state, args, annotation_id);
767 if use_display_units {
768 let set_units_id = exec_state.next_uuid();
769 let reset_units_id = exec_state.next_uuid();
770 exec_state
771 .batch_modeling_cmds(
772 cmd_meta,
773 &[
774 set_engine_scene_units_cmd(set_units_id, display_units),
775 ModelingCmdReq {
776 cmd_id: annotation_id.into(),
777 cmd: annotation_cmd,
778 },
779 set_engine_scene_units_cmd(reset_units_id, kcmc::units::UnitLength::Millimeters),
780 ],
781 )
782 .await?;
783 } else {
784 exec_state.batch_modeling_cmd(cmd_meta, annotation_cmd).await?;
785 }
786 add_gdt_annotation_artifact(exec_state, args, annotation_id);
787 annotations.push(GdtAnnotation {
788 id: annotation_id,
789 meta,
790 });
791 Ok(())
792}
793
794pub async fn perpendicularity(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
795 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
796 "faces",
797 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
798 exec_state,
799 )?;
800 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
801 "edges",
802 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
803 exec_state,
804 )?;
805 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
806 "datums",
807 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
808 exec_state,
809 )?;
810 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
811 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
812 let frame_position: Option<[TyF64; 2]> =
813 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
814 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
815 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
816 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
817
818 let annotations = inner_perpendicularity(
819 faces.unwrap_or_default(),
820 edges.unwrap_or_default(),
821 datums,
822 tolerance,
823 precision,
824 frame_position,
825 frame_plane,
826 leader_scale,
827 font_size,
828 exec_state,
829 &args,
830 )
831 .await?;
832 Ok(annotations.into())
833}
834
835#[allow(clippy::too_many_arguments)]
836async fn inner_perpendicularity(
837 faces: Vec<TagIdentifier>,
838 edges: Vec<EdgeReference>,
839 datums: Option<Vec<String>>,
840 tolerance: TyF64,
841 precision: Option<TyF64>,
842 frame_position: Option<[TyF64; 2]>,
843 frame_plane: Option<Plane>,
844 leader_scale: Option<TyF64>,
845 font_size: Option<TyF64>,
846 exec_state: &mut ExecState,
847 args: &Args,
848) -> Result<Vec<GdtAnnotation>, KclError> {
849 if faces.is_empty() && edges.is_empty() {
850 return Err(KclError::new_semantic(KclErrorDetails::new(
851 "Perpendicularity requires at least one face or edge.".to_owned(),
852 vec![args.source_range],
853 )));
854 }
855
856 let precision = resolve_precision(precision, args)?;
857 let datums = resolve_datums(datums, args, "Perpendicularity")?;
858 let mut frame_plane = if let Some(plane) = frame_plane {
859 plane
860 } else {
861 xy_plane(exec_state, args).await?
862 };
863 ensure_sketch_plane_in_engine(
864 &mut frame_plane,
865 exec_state,
866 &args.ctx,
867 args.source_range,
868 args.node_path.clone(),
869 )
870 .await?;
871
872 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
873 for face in &faces {
874 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
875 create_feature_control_annotation(
876 face_id,
877 MbdSymbol::Perpendicularity,
878 &tolerance,
879 &datums,
880 precision,
881 frame_position.as_ref(),
882 frame_plane.id,
883 leader_scale.as_ref(),
884 font_size.as_ref(),
885 exec_state,
886 args,
887 &mut annotations,
888 )
889 .await?;
890 }
891 for edge in &edges {
892 let edge_id = edge.get_engine_id(exec_state, args)?;
893 create_feature_control_annotation(
894 edge_id,
895 MbdSymbol::Perpendicularity,
896 &tolerance,
897 &datums,
898 precision,
899 frame_position.as_ref(),
900 frame_plane.id,
901 leader_scale.as_ref(),
902 font_size.as_ref(),
903 exec_state,
904 args,
905 &mut annotations,
906 )
907 .await?;
908 }
909
910 Ok(annotations)
911}
912
913pub async fn parallelism(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
914 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
915 "faces",
916 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
917 exec_state,
918 )?;
919 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
920 "edges",
921 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
922 exec_state,
923 )?;
924 let datums: Option<Vec<String>> = args.get_kw_arg_opt(
925 "datums",
926 &RuntimeType::Array(Box::new(RuntimeType::string()), ArrayLen::Minimum(1)),
927 exec_state,
928 )?;
929 let tolerance = args.get_kw_arg("tolerance", &RuntimeType::length(), exec_state)?;
930 let precision = args.get_kw_arg_opt("precision", &RuntimeType::count(), exec_state)?;
931 let frame_position: Option<[TyF64; 2]> =
932 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
933 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
934 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
935 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
936
937 let annotations = inner_parallelism(
938 faces.unwrap_or_default(),
939 edges.unwrap_or_default(),
940 datums,
941 tolerance,
942 precision,
943 frame_position,
944 frame_plane,
945 leader_scale,
946 font_size,
947 exec_state,
948 &args,
949 )
950 .await?;
951 Ok(annotations.into())
952}
953
954#[allow(clippy::too_many_arguments)]
955async fn inner_parallelism(
956 faces: Vec<TagIdentifier>,
957 edges: Vec<EdgeReference>,
958 datums: Option<Vec<String>>,
959 tolerance: TyF64,
960 precision: Option<TyF64>,
961 frame_position: Option<[TyF64; 2]>,
962 frame_plane: Option<Plane>,
963 leader_scale: Option<TyF64>,
964 font_size: Option<TyF64>,
965 exec_state: &mut ExecState,
966 args: &Args,
967) -> Result<Vec<GdtAnnotation>, KclError> {
968 if faces.is_empty() && edges.is_empty() {
969 return Err(KclError::new_semantic(KclErrorDetails::new(
970 "Parallelism requires at least one face or edge.".to_owned(),
971 vec![args.source_range],
972 )));
973 }
974
975 let precision = resolve_precision(precision, args)?;
976 let datums = resolve_datums(datums, args, "Parallelism")?;
977 let mut frame_plane = if let Some(plane) = frame_plane {
978 plane
979 } else {
980 xy_plane(exec_state, args).await?
981 };
982 ensure_sketch_plane_in_engine(
983 &mut frame_plane,
984 exec_state,
985 &args.ctx,
986 args.source_range,
987 args.node_path.clone(),
988 )
989 .await?;
990
991 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
992 for face in &faces {
993 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
994 create_feature_control_annotation(
995 face_id,
996 MbdSymbol::Parallelism,
997 &tolerance,
998 &datums,
999 precision,
1000 frame_position.as_ref(),
1001 frame_plane.id,
1002 leader_scale.as_ref(),
1003 font_size.as_ref(),
1004 exec_state,
1005 args,
1006 &mut annotations,
1007 )
1008 .await?;
1009 }
1010 for edge in &edges {
1011 let edge_id = edge.get_engine_id(exec_state, args)?;
1012 create_feature_control_annotation(
1013 edge_id,
1014 MbdSymbol::Parallelism,
1015 &tolerance,
1016 &datums,
1017 precision,
1018 frame_position.as_ref(),
1019 frame_plane.id,
1020 leader_scale.as_ref(),
1021 font_size.as_ref(),
1022 exec_state,
1023 args,
1024 &mut annotations,
1025 )
1026 .await?;
1027 }
1028
1029 Ok(annotations)
1030}
1031
1032pub async fn annotation(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1033 let annotation: String = args.get_kw_arg("annotation", &RuntimeType::string(), exec_state)?;
1034 let faces: Option<Vec<TagIdentifier>> = args.get_kw_arg_opt(
1035 "faces",
1036 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Minimum(1)),
1037 exec_state,
1038 )?;
1039 let edges: Option<Vec<EdgeReference>> = args.get_kw_arg_opt(
1040 "edges",
1041 &RuntimeType::Array(Box::new(RuntimeType::edge()), ArrayLen::Minimum(1)),
1042 exec_state,
1043 )?;
1044 let frame_position: Option<[TyF64; 2]> =
1045 args.get_kw_arg_opt("framePosition", &RuntimeType::point2d(), exec_state)?;
1046 let frame_plane: Option<Plane> = args.get_kw_arg_opt("framePlane", &RuntimeType::plane(), exec_state)?;
1047 let leader_scale: Option<TyF64> = args.get_kw_arg_opt("leaderScale", &RuntimeType::count(), exec_state)?;
1048 let font_size: Option<TyF64> = args.get_kw_arg_opt("fontSize", &RuntimeType::length(), exec_state)?;
1049
1050 let annotations = inner_annotation(
1051 annotation,
1052 faces.unwrap_or_default(),
1053 edges.unwrap_or_default(),
1054 frame_position,
1055 frame_plane,
1056 leader_scale,
1057 font_size,
1058 exec_state,
1059 &args,
1060 )
1061 .await?;
1062 Ok(annotations.into())
1063}
1064
1065#[allow(clippy::too_many_arguments)]
1066async fn inner_annotation(
1067 annotation: String,
1068 faces: Vec<TagIdentifier>,
1069 edges: Vec<EdgeReference>,
1070 frame_position: Option<[TyF64; 2]>,
1071 frame_plane: Option<Plane>,
1072 leader_scale: Option<TyF64>,
1073 font_size: Option<TyF64>,
1074 exec_state: &mut ExecState,
1075 args: &Args,
1076) -> Result<Vec<GdtAnnotation>, KclError> {
1077 if annotation.is_empty() {
1078 return Err(KclError::new_semantic(KclErrorDetails::new(
1079 "Annotation text must not be empty.".to_owned(),
1080 vec![args.source_range],
1081 )));
1082 }
1083 if faces.is_empty() && edges.is_empty() {
1084 return Err(KclError::new_semantic(KclErrorDetails::new(
1085 "Annotation requires at least one face or edge.".to_owned(),
1086 vec![args.source_range],
1087 )));
1088 }
1089
1090 let mut frame_plane = if let Some(plane) = frame_plane {
1091 plane
1092 } else {
1093 xy_plane(exec_state, args).await?
1094 };
1095 ensure_sketch_plane_in_engine(
1096 &mut frame_plane,
1097 exec_state,
1098 &args.ctx,
1099 args.source_range,
1100 args.node_path.clone(),
1101 )
1102 .await?;
1103
1104 let mut annotations = Vec::with_capacity(faces.len() + edges.len());
1105 for face in &faces {
1106 let face_id = args.get_adjacent_face_to_tag(exec_state, face, false).await?;
1107 create_annotation(
1108 face_id,
1109 &annotation,
1110 frame_position.as_ref(),
1111 frame_plane.id,
1112 leader_scale.as_ref(),
1113 font_size.as_ref(),
1114 exec_state,
1115 args,
1116 &mut annotations,
1117 )
1118 .await?;
1119 }
1120 for edge in &edges {
1121 let edge_id = edge.get_engine_id(exec_state, args)?;
1122 create_annotation(
1123 edge_id,
1124 &annotation,
1125 frame_position.as_ref(),
1126 frame_plane.id,
1127 leader_scale.as_ref(),
1128 font_size.as_ref(),
1129 exec_state,
1130 args,
1131 &mut annotations,
1132 )
1133 .await?;
1134 }
1135
1136 Ok(annotations)
1137}
1138
1139fn resolve_precision(precision: Option<TyF64>, args: &Args) -> Result<u32, KclError> {
1140 if let Some(precision) = precision {
1141 let rounded = precision.n.round();
1142 if !(0.0..=9.0).contains(&rounded) {
1143 return Err(KclError::new_semantic(KclErrorDetails::new(
1144 "Precision must be between 0 and 9".to_owned(),
1145 vec![args.source_range],
1146 )));
1147 }
1148 Ok(rounded as u32)
1149 } else {
1150 Ok(3)
1151 }
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
1213fn gdt_control_frame(symbol: MbdSymbol, tolerance: f64, datums: &[char]) -> AnnotationMbdControlFrame {
1214 match datums {
1215 [] => AnnotationMbdControlFrame::builder()
1216 .symbol(symbol)
1217 .tolerance(tolerance)
1218 .build(),
1219 [primary] => AnnotationMbdControlFrame::builder()
1220 .symbol(symbol)
1221 .tolerance(tolerance)
1222 .primary_datum(*primary)
1223 .build(),
1224 [primary, secondary] => AnnotationMbdControlFrame::builder()
1225 .symbol(symbol)
1226 .tolerance(tolerance)
1227 .primary_datum(*primary)
1228 .secondary_datum(*secondary)
1229 .build(),
1230 [primary, secondary, tertiary] => AnnotationMbdControlFrame::builder()
1231 .symbol(symbol)
1232 .tolerance(tolerance)
1233 .primary_datum(*primary)
1234 .secondary_datum(*secondary)
1235 .tertiary_datum(*tertiary)
1236 .build(),
1237 _ => unreachable!("resolve_datums rejects more than three datums"),
1238 }
1239}
1240
1241#[allow(clippy::too_many_arguments)]
1242async fn create_annotation(
1243 entity_id: uuid::Uuid,
1244 annotation: &str,
1245 frame_position: Option<&[TyF64; 2]>,
1246 frame_plane_id: uuid::Uuid,
1247 leader_scale: Option<&TyF64>,
1248 font_size: Option<&TyF64>,
1249 exec_state: &mut ExecState,
1250 args: &Args,
1251 annotations: &mut Vec<GdtAnnotation>,
1252) -> Result<(), KclError> {
1253 let meta = vec![Metadata::from(args.source_range)];
1254 let annotation_id = exec_state.next_uuid();
1255 let feature_control = AnnotationFeatureControl::builder()
1256 .entity_id(entity_id)
1257 .entity_pos(KPoint2d { x: 0.5, y: 0.5 })
1258 .leader_type(AnnotationLineEnd::Dot)
1259 .prefix(annotation.to_owned())
1260 .plane_id(frame_plane_id)
1261 .offset(if let Some(offset) = frame_position {
1262 KPoint2d {
1263 x: offset[0].to_mm(),
1264 y: offset[1].to_mm(),
1265 }
1266 } else {
1267 KPoint2d { x: 100.0, y: 100.0 }
1268 })
1269 .precision(0)
1270 .font_scale(gdt_font_scale(font_size, args)?)
1271 .font_point_size(GDT_FONT_TEXTURE_POINT_SIZE)
1272 .leader_scale(gdt_dot_leader_scale(leader_scale, font_size, args)?)
1273 .build();
1274 let options = AnnotationOptions::builder().feature_control(feature_control).build();
1275 exec_state
1276 .batch_modeling_cmd(
1277 ModelingCmdMeta::from_args_id(exec_state, args, annotation_id),
1278 ModelingCmd::from(
1279 mcmd::NewAnnotation::builder()
1280 .options(options)
1281 .clobber(false)
1282 .annotation_type(AnnotationType::T3D)
1283 .build(),
1284 ),
1285 )
1286 .await?;
1287 add_gdt_annotation_artifact(exec_state, args, annotation_id);
1288 annotations.push(GdtAnnotation {
1289 id: annotation_id,
1290 meta,
1291 });
1292 Ok(())
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}