1use kcl_api::UnitLength;
4
5use crate::SourceRange;
6use crate::errors::KclError;
7use crate::errors::KclErrorDetails;
8use crate::execution::Artifact;
9use crate::execution::ArtifactId;
10use crate::execution::CameraView;
11use crate::execution::CodeRef;
12use crate::execution::ExecState;
13use crate::execution::KclValue;
14use crate::execution::NamedViewValue;
15use crate::execution::Orientation;
16use crate::execution::Point3d;
17use crate::execution::Projection;
18use crate::execution::Visibility;
19use crate::execution::named_view_artifact;
20use crate::execution::types::NumericType;
21use crate::execution::types::NumericTypeExt;
22use crate::execution::types::RuntimeType;
23use crate::std::Args;
24use crate::std::args::TyF64;
25
26pub async fn oriented(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
28 let orientation: Orientation = args.get_unlabeled_kw_arg("orientation", &RuntimeType::any(), exec_state)?;
35 let target: Option<[TyF64; 3]> = args.get_kw_arg_opt("target", &RuntimeType::point3d(), exec_state)?;
36 let distance: Option<TyF64> = args.get_kw_arg_opt("distance", &RuntimeType::length(), exec_state)?;
37 let projection: Option<Projection> = args.get_kw_arg_opt("projection", &RuntimeType::any(), exec_state)?;
38
39 let view = CameraView::oriented(
40 orientation,
41 target.map(millimeter_point),
42 distance.map(millimeter_length),
43 projection,
44 vec![args.source_range.into()],
45 )
46 .map_err(|err| view_error(err, &args))?;
47 Ok(KclValue::CameraView { value: Box::new(view) })
48}
49
50pub async fn directed(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
52 let direction: [TyF64; 3] = args.get_unlabeled_kw_arg("direction", &RuntimeType::point3d(), exec_state)?;
59 let up: Option<[TyF64; 3]> = args.get_kw_arg_opt("up", &RuntimeType::point3d(), exec_state)?;
60 let target: Option<[TyF64; 3]> = args.get_kw_arg_opt("target", &RuntimeType::point3d(), exec_state)?;
61 let distance: Option<TyF64> = args.get_kw_arg_opt("distance", &RuntimeType::length(), exec_state)?;
62 let projection: Option<Projection> = args.get_kw_arg_opt("projection", &RuntimeType::any(), exec_state)?;
63
64 let view = CameraView::directed(
65 unitless_direction(direction),
66 up.map(unitless_direction),
67 target.map(millimeter_point),
68 distance.map(millimeter_length),
69 projection,
70 vec![args.source_range.into()],
71 )
72 .map_err(|err| view_error(err, &args))?;
73 Ok(KclValue::CameraView { value: Box::new(view) })
74}
75
76pub async fn named(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
78 let name: String = args.get_unlabeled_kw_arg("name", &RuntimeType::string(), exec_state)?;
81 let camera: CameraView = args.get_kw_arg("camera", &RuntimeType::any(), exec_state)?;
82 let baseline: Visibility = args.get_kw_arg("baseline", &RuntimeType::any(), exec_state)?;
83 let except: Option<Vec<KclValue>> = args.get_kw_arg_opt("except", &RuntimeType::any(), exec_state)?;
84
85 let except_ids = except
86 .as_ref()
87 .map(|objects| except_artifact_ids(objects, args.source_range))
88 .transpose()?;
89
90 let artifact_id = exec_state.next_artifact_id();
93 let view = NamedViewValue::new(
94 artifact_id,
95 name,
96 camera,
97 baseline,
98 except_ids,
99 args.source_range.module_id(),
100 exec_state.registered_named_views(),
101 vec![args.source_range.into()],
102 )
103 .map_err(|err| view_error(err, &args))?;
104 exec_state.add_artifact(Artifact::NamedView(named_view_artifact(
107 &view,
108 CodeRef::placeholder(args.source_range),
109 )));
110
111 Ok(KclValue::NamedView { value: Box::new(view) })
112}
113
114fn except_artifact_ids(objects: &[KclValue], source_range: SourceRange) -> Result<Vec<ArtifactId>, KclError> {
121 objects
122 .iter()
123 .map(|object| match object {
124 KclValue::Solid { value } => Ok(value.artifact_id),
125 KclValue::Sketch { value } => Ok(value.artifact_id),
126 KclValue::GdtAnnotation { value } => Ok(ArtifactId::new(value.id)),
127 KclValue::Helix { value } => Ok(value.artifact_id),
128 KclValue::Plane { value } => {
129 if value.is_standard() || value.is_uninitialized() {
130 Err(KclError::new_semantic(KclErrorDetails::new(
131 "Named views cannot control default planes or other uninitialized planes because their ids do not identify independent engine objects. Use a standalone plane returned by `offsetPlane()`."
132 .to_owned(),
133 vec![source_range],
134 )))
135 } else {
136 Ok(value.artifact_id)
137 }
138 }
139 KclValue::ImportedGeometry(value) => Ok(ArtifactId::new(value.id)),
140 other => Err(KclError::new_internal(KclErrorDetails::new(
141 format!(
142 "`except` cannot hold {}; the declared signature should have rejected it",
143 other.human_friendly_type()
144 ),
145 vec![source_range],
146 ))),
147 })
148 .collect()
149}
150
151fn view_error<E: std::fmt::Display>(err: E, args: &Args) -> KclError {
159 KclError::new_semantic(KclErrorDetails::new(err.to_string(), vec![args.source_range]))
160}
161
162fn millimeter_point([x, y, z]: [TyF64; 3]) -> Point3d {
180 Point3d {
181 x: x.to_mm(),
182 y: y.to_mm(),
183 z: z.to_mm(),
184 units: Some(UnitLength::Millimeters),
185 }
186}
187
188fn millimeter_length(length: TyF64) -> TyF64 {
190 TyF64::new(length.to_mm(), NumericType::mm())
191}
192
193fn unitless_direction([x, y, z]: [TyF64; 3]) -> Point3d {
200 Point3d {
201 x: x.to_mm(),
202 y: y.to_mm(),
203 z: z.to_mm(),
204 units: None,
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use crate::execution::ArtifactId;
211 use crate::execution::KclValue;
212 use crate::execution::Visibility;
213 use crate::execution::parse_execute;
214
215 const TWO_SOLIDS: &str = r#"@settings(experimentalFeatures = allow)
220
221plateSketch = sketch(on = XY) {
222 edge1 = line(start = [var 0mm, var 0mm], end = [var 40mm, var 0mm])
223 edge2 = line(start = [var 40mm, var 0mm], end = [var 40mm, var 20mm])
224 edge3 = line(start = [var 40mm, var 20mm], end = [var 0mm, var 20mm])
225 edge4 = line(start = [var 0mm, var 20mm], end = [var 0mm, var 0mm])
226 coincident([edge1.end, edge2.start])
227 coincident([edge2.end, edge3.start])
228 coincident([edge3.end, edge4.start])
229 coincident([edge4.end, edge1.start])
230}
231plateRegion = region(point = [20mm, 10mm], sketch = plateSketch)
232plate = extrude(plateRegion, length = 5mm)
233
234bossSketch = sketch(on = XY) {
235 edge1 = line(start = [var 50mm, var 0mm], end = [var 60mm, var 0mm])
236 edge2 = line(start = [var 60mm, var 0mm], end = [var 60mm, var 10mm])
237 edge3 = line(start = [var 60mm, var 10mm], end = [var 50mm, var 10mm])
238 edge4 = line(start = [var 50mm, var 10mm], end = [var 50mm, var 0mm])
239 coincident([edge1.end, edge2.start])
240 coincident([edge2.end, edge3.start])
241 coincident([edge3.end, edge4.start])
242 coincident([edge4.end, edge1.start])
243}
244bossRegion = region(point = [55mm, 5mm], sketch = bossSketch)
245boss = extrude(bossRegion, length = 8mm)
246"#;
247
248 #[tokio::test(flavor = "multi_thread")]
251 async fn named_shows_everything_under_a_show_baseline() {
252 let program = format!(
253 "{TWO_SOLIDS}\nv = view::named(\n \"Overview\",\n camera = view::oriented(view::Orientation::Isometric),\n baseline = view::Visibility::Show,\n)\n"
254 );
255 let result = parse_execute(&program).await.expect("the program executes");
256
257 let KclValue::NamedView { value } = result.variable("v") else {
258 panic!("`v` is not a named view");
259 };
260 assert_eq!(value.name(), "Overview");
261 assert_eq!(value.baseline(), Visibility::Show);
262 assert!(value.except_ids().is_empty());
263 }
264
265 #[tokio::test(flavor = "multi_thread")]
269 async fn named_excepts_the_objects_it_is_given() {
270 let program = format!(
271 "{TWO_SOLIDS}\nv = view::named(\n \"Plate only\",\n camera = view::oriented(view::Orientation::Front),\n baseline = view::Visibility::Hide,\n except = [plate, boss, plate],\n)\n"
272 );
273 let result = parse_execute(&program).await.expect("the program executes");
274
275 let KclValue::Solid { value: plate } = result.variable("plate") else {
276 panic!("`plate` is not a solid");
277 };
278 let KclValue::Solid { value: boss } = result.variable("boss") else {
279 panic!("`boss` is not a solid");
280 };
281 let KclValue::NamedView { value } = result.variable("v") else {
282 panic!("`v` is not a named view");
283 };
284
285 assert_eq!(value.baseline(), Visibility::Hide);
286 assert_eq!(value.except_ids().to_vec(), vec![plate.artifact_id, boss.artifact_id]);
288 }
289
290 #[tokio::test(flavor = "multi_thread")]
295 async fn named_excepts_more_than_one_kind() {
296 let program = format!(
297 "{TWO_SOLIDS}\nnote = gdt::note(note = \"Machine after welding\")\nv = view::named(\n \"Mixed\",\n camera = view::oriented(view::Orientation::Top),\n baseline = view::Visibility::Hide,\n except = [plate, bossRegion, note],\n)\n"
298 );
299 let result = parse_execute(&program).await.expect("the program executes");
300
301 let KclValue::Solid { value: plate } = result.variable("plate") else {
302 panic!("`plate` is not a solid");
303 };
304 let KclValue::Sketch { value: sketch } = result.variable("bossRegion") else {
308 panic!("`bossRegion` is not a sketch");
309 };
310 let KclValue::GdtAnnotation { value: note } = result.variable("note") else {
311 panic!("`note` is not an annotation");
312 };
313 let KclValue::NamedView { value } = result.variable("v") else {
314 panic!("`v` is not a named view");
315 };
316
317 assert_eq!(
318 value.except_ids().to_vec(),
319 vec![plate.artifact_id, sketch.artifact_id, ArtifactId::new(note.id)]
320 );
321 }
322
323 #[tokio::test(flavor = "multi_thread")]
327 async fn named_excepts_a_custom_plane_and_helix_by_their_artifact_ids() {
328 let program = r#"@settings(experimentalFeatures = allow)
329inspectionPlane = offsetPlane(XY, offset = 20mm)
330spring = helix(
331 axis = Z,
332 radius = 5mm,
333 length = 20mm,
334 revolutions = 4,
335 angleStart = 0deg,
336)
337v = view::named(
338 "Construction geometry",
339 camera = view::oriented(view::Orientation::Isometric),
340 baseline = view::Visibility::Hide,
341 except = [inspectionPlane, spring],
342)
343"#;
344 let result = parse_execute(program).await.expect("the program executes");
345
346 let KclValue::Plane { value: plane } = result.variable("inspectionPlane") else {
347 panic!("`inspectionPlane` is not a plane");
348 };
349 let KclValue::Helix { value: helix } = result.variable("spring") else {
350 panic!("`spring` is not a helix");
351 };
352 let KclValue::NamedView { value } = result.variable("v") else {
353 panic!("`v` is not a named view");
354 };
355
356 assert!(plane.is_initialized());
357 assert_eq!(value.except_ids().to_vec(), vec![plane.artifact_id, helix.artifact_id]);
358 }
359
360 #[test]
363 fn named_excepts_imported_geometry_by_its_artifact_id() {
364 let id = uuid::Uuid::from_u128(1);
365 let imported = KclValue::ImportedGeometry(crate::execution::ImportedGeometry::new(
366 id,
367 vec!["part.step".to_owned()],
368 Vec::new(),
369 ));
370
371 assert_eq!(
372 super::except_artifact_ids(&[imported], crate::SourceRange::default()).expect("the value is accepted"),
373 vec![ArtifactId::new(id)]
374 );
375 }
376
377 async fn execution_error(code: &str) -> String {
387 let program = format!("@settings(experimentalFeatures = allow)\n{code}");
388 match parse_execute(&program).await {
389 Ok(_) => panic!("expected `{code}` to be rejected, but it executed"),
390 Err(err) => err.message().to_owned(),
391 }
392 }
393
394 async fn issues_without_opt_in(code: &str) -> Vec<String> {
399 let result = parse_execute(code).await.expect("experimental use is not fatal");
400 result.issues().iter().map(|issue| issue.message.clone()).collect()
401 }
402
403 #[tokio::test(flavor = "multi_thread")]
406 async fn rejected_arguments_report_their_reason() {
407 assert_eq!(
409 execution_error("v = view::directed([1 / 0, 0, 0])").await,
410 "`direction` must have finite coordinates."
411 );
412 assert_eq!(
413 execution_error("v = view::directed([0, 1, 0], up = [0, 0, 1 / 0])").await,
414 "`up` must have finite coordinates."
415 );
416 assert_eq!(
417 execution_error("v = view::directed([0, 1, 0], target = [1 / 0, 0, 0])").await,
418 "`target` must have finite coordinates."
419 );
420 assert_eq!(
421 execution_error("v = view::oriented(view::Orientation::Front, target = [0, 1 / 0, 0])").await,
422 "`target` must have finite coordinates."
423 );
424 assert_eq!(
425 execution_error("v = view::oriented(view::Orientation::Front, distance = 1 / 0)").await,
426 "`distance` must be a finite number."
427 );
428 assert_eq!(
429 execution_error("v = view::directed([0, 1, 0], distance = 0)").await,
430 "`distance` must be greater than zero."
431 );
432 assert_eq!(
433 execution_error("v = view::oriented(view::Orientation::Front, distance = -50)").await,
434 "`distance` must be greater than zero."
435 );
436 assert_eq!(
437 execution_error("v = view::directed([0, 0, 0])").await,
438 "`direction` must be a non-zero vector."
439 );
440 assert_eq!(
441 execution_error("v = view::directed([0, 1, 0], up = [0, 0, 0])").await,
442 "`up` must be a non-zero vector."
443 );
444 assert_eq!(
445 execution_error("v = view::directed([0, 0, 1])").await,
446 "`direction` and `up` must not be parallel or nearly parallel."
447 );
448 }
449
450 #[tokio::test(flavor = "multi_thread")]
455 async fn named_rejects_names_it_cannot_identify_a_view_by() {
456 let showing = "camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show";
458
459 assert_eq!(
460 execution_error(&format!(r#"v = view::named("", {showing})"#)).await,
461 "A view's name must not be empty."
462 );
463 assert_eq!(
464 execution_error(&format!(r#"v = view::named(" ", {showing})"#)).await,
465 "A view's name must not be only whitespace."
466 );
467 assert_eq!(
468 execution_error(&format!(r#"v = view::named("Front ", {showing})"#)).await,
469 "A view's name must not start or end with whitespace. Use `string::trim()` to remove it."
470 );
471 assert_eq!(
472 execution_error(&format!(r#"v = view::named(" Front", {showing})"#)).await,
473 "A view's name must not start or end with whitespace. Use `string::trim()` to remove it."
474 );
475 assert_eq!(
476 execution_error(&format!(r#"v = view::named("Default View", {showing})"#)).await,
477 "`Default View` is reserved for the view of the scene generated on successful execution of the program. Please give this view a different name."
478 );
479 assert_eq!(
480 execution_error(&format!(
481 "a = view::named(\"Front\", {showing})\nb = view::named(\"Front\", {showing})"
482 ))
483 .await,
484 "A view named `Front` already exists. Every view needs its own name, and names are compared exactly, so `Front` and `front` are different names."
485 );
486 }
487
488 #[tokio::test(flavor = "multi_thread")]
492 async fn named_accepts_names_that_differ_only_in_case() {
493 let showing = "camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show";
494 let program = format!(
495 "@settings(experimentalFeatures = allow)\na = view::named(\"Front\", {showing})\nb = view::named(\"front\", {showing})\n"
496 );
497 let result = parse_execute(&program).await.expect("the program executes");
498
499 let KclValue::NamedView { value: upper } = result.variable("a") else {
500 panic!("`a` is not a named view");
501 };
502 let KclValue::NamedView { value: lower } = result.variable("b") else {
503 panic!("`b` is not a named view");
504 };
505 assert_eq!(upper.name(), "Front");
506 assert_eq!(lower.name(), "front");
507 }
508
509 #[tokio::test(flavor = "multi_thread")]
514 async fn named_requires_a_baseline() {
515 assert_eq!(
516 execution_error(r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front))"#).await,
517 "The `view::named` function requires a keyword argument `baseline`"
518 );
519 }
520
521 #[tokio::test(flavor = "multi_thread")]
526 async fn named_rejects_wrongly_typed_arguments() {
527 assert_eq!(
529 execution_error(r#"v = view::named("Front", baseline = view::Visibility::Show)"#).await,
530 "The `view::named` function requires a keyword argument `camera`"
531 );
532 assert_eq!(
536 execution_error(r#"v = view::named("Front", camera = "nope", baseline = view::Visibility::Show)"#).await,
537 "camera requires a value with type `CameraView`, but found a value with type `string`."
538 );
539 assert_eq!(
542 execution_error(
543 r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Orientation::Top)"#
544 )
545 .await,
546 "baseline requires a value with type `Visibility`, but found a value of enum `Orientation` (with type `Orientation`)."
547 );
548 assert_eq!(
550 execution_error(
551 r#"v = view::named(view::Visibility::Show, camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show)"#
552 )
553 .await,
554 "The input argument of `view::named` requires a value with type `string`, but found a value of enum `Visibility` (with type `Visibility`)."
555 );
556 }
557
558 #[tokio::test(flavor = "multi_thread")]
563 async fn declaring_a_view_requires_the_experimental_opt_in() {
564 assert!(
565 issues_without_opt_in(
566 r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show)"#
567 )
568 .await
569 .contains(&"Use of `view::named` is experimental and may change or be removed.".to_owned())
570 );
571 }
572
573 #[tokio::test(flavor = "multi_thread")]
580 async fn named_excepts_a_sketch_block_by_its_own_id() {
581 let program = format!(
582 "{TWO_SOLIDS}\nv = view::named(\n \"Block\",\n camera = view::oriented(view::Orientation::Front),\n baseline = view::Visibility::Hide,\n except = [plateSketch],\n)\n"
583 );
584 let result = parse_execute(&program).await.expect("the program executes");
585
586 let KclValue::Sketch { value: region } = result.variable("plateRegion") else {
587 panic!("`plateRegion` is not a sketch");
588 };
589 let KclValue::NamedView { value } = result.variable("v") else {
590 panic!("`v` is not a named view");
591 };
592
593 assert_eq!(value.except_ids().len(), 1);
594 assert_ne!(
595 value.except_ids()[0],
596 region.artifact_id,
597 "the block and the region taken from it are different artifacts"
598 );
599 }
600
601 #[tokio::test(flavor = "multi_thread")]
605 async fn named_rejects_an_empty_except_list() {
606 assert_eq!(
607 execution_error(
608 r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Hide, except = [])"#
609 )
610 .await,
611 "except requires one or more `Solid`s or `Sketch`s or `GdtAnnotation`s or `Helix`s or `Plane`s or imported geometries (`[Solid | Sketch | GdtAnnotation | Helix | Plane | ImportedGeometry; 1+]`), but found an empty array (with type `[any; 0]`)."
612 );
613 }
614
615 #[tokio::test(flavor = "multi_thread")]
618 async fn named_rejects_every_default_plane() {
619 for plane in ["XY", "XZ", "YZ", "-XY", "-XZ", "-YZ"] {
620 assert_eq!(
621 execution_error(&format!(
622 "v = view::named(\"Front\", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Hide, except = [{plane}])"
623 ))
624 .await,
625 "Named views cannot control default planes or other uninitialized planes because their ids do not identify independent engine objects. Use a standalone plane returned by `offsetPlane()`."
626 );
627 }
628 }
629
630 #[tokio::test(flavor = "multi_thread")]
633 async fn named_rejects_an_uninitialized_custom_plane() {
634 assert_eq!(
635 execution_error(
636 r#"customPlane = {
637 origin = { x = 0, y = 0, z = 0 },
638 xAxis = { x = 1, y = 0, z = 0 },
639 yAxis = { x = 0, y = 1, z = 0 },
640}
641v = view::named(
642 "Front",
643 camera = view::oriented(view::Orientation::Front),
644 baseline = view::Visibility::Hide,
645 except = [customPlane],
646)"#
647 )
648 .await,
649 "Named views cannot control default planes or other uninitialized planes because their ids do not identify independent engine objects. Use a standalone plane returned by `offsetPlane()`."
650 );
651 }
652
653 #[tokio::test(flavor = "multi_thread")]
665 async fn wrongly_typed_arguments_are_rejected() {
666 assert_eq!(
668 execution_error(r#"v = view::oriented("Front")"#).await,
669 "The input argument of `view::oriented` requires a value with type `Orientation`, but found a value with type `string`."
670 );
671 assert_eq!(
674 execution_error("v = view::oriented(view::Projection::Perspective)").await,
675 "The input argument of `view::oriented` requires a value with type `Orientation`, but found a value of enum `Projection` (with type `Projection`)."
676 );
677 assert_eq!(
680 execution_error("type MyOrientation { | Front }\nv = view::oriented(MyOrientation::Front)").await,
681 "The input argument of `view::oriented` requires a value with type `Orientation`, but found a value of enum `MyOrientation` (with type `MyOrientation`)."
682 );
683 assert_eq!(
685 execution_error("v = view::oriented(view::Orientation::Front, projection = view::Orientation::Top)").await,
686 "projection requires a value with type `Projection`, but found a value of enum `Orientation` (with type `Orientation`)."
687 );
688 assert_eq!(
690 execution_error(r#"v = view::directed("nope")"#).await,
691 "The input argument of `view::directed` requires a value with type `Point3d`, but found a value with type `string`."
692 );
693 }
694
695 #[tokio::test(flavor = "multi_thread")]
705 async fn calling_a_constructor_requires_the_experimental_opt_in() {
706 assert!(
707 issues_without_opt_in("v = view::directed([0, 1, -2])")
708 .await
709 .contains(&"Use of `view::directed` is experimental and may change or be removed.".to_owned())
710 );
711
712 assert!(
715 issues_without_opt_in("v = view::oriented(view::Orientation::Front)")
716 .await
717 .contains(&"Use of `view::oriented` is experimental and may change or be removed.".to_owned())
718 );
719 }
720
721 #[tokio::test(flavor = "multi_thread")]
725 async fn opaque_types_resolve_in_signatures() {
726 let code = r#"@settings(experimentalFeatures = allow)
727import CameraView, NamedView from "std::view"
728
729fn acceptsCamera(@camera: CameraView) {
730 return 0
731}
732
733fn passesNamed(@input: NamedView): NamedView {
734 return input
735}
736"#;
737 if let Err(err) = parse_execute(code).await {
738 panic!("expected the declarations to resolve, but got: {}", err.message());
739 }
740 }
741
742 #[tokio::test(flavor = "multi_thread")]
746 async fn qualified_type_path_resolves_in_signature() {
747 let code = r#"@settings(experimentalFeatures = allow)
748fn passesOrientation(@input: view::Orientation): view::Orientation {
749 return input
750}
751"#;
752 if let Err(err) = parse_execute(code).await {
753 panic!("expected the declarations to resolve, but got: {}", err.message());
754 }
755 }
756}