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