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