1use kcl_api::UnitLength;
4
5use crate::errors::KclError;
6use crate::errors::KclErrorDetails;
7use crate::execution::Artifact;
8use crate::execution::ArtifactId;
9use crate::execution::CameraView;
10use crate::execution::CodeRef;
11use crate::execution::ExecState;
12use crate::execution::KclValue;
13use crate::execution::NamedViewValue;
14use crate::execution::Orientation;
15use crate::execution::Point3d;
16use crate::execution::Projection;
17use crate::execution::Visibility;
18use crate::execution::named_view_artifact;
19use crate::execution::types::NumericType;
20use crate::execution::types::NumericTypeExt;
21use crate::execution::types::RuntimeType;
22use crate::std::Args;
23use crate::std::args::TyF64;
24
25pub async fn oriented(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
27 let orientation: Orientation = args.get_unlabeled_kw_arg("orientation", &RuntimeType::any(), exec_state)?;
34 let target: Option<[TyF64; 3]> = args.get_kw_arg_opt("target", &RuntimeType::point3d(), exec_state)?;
35 let distance: Option<TyF64> = args.get_kw_arg_opt("distance", &RuntimeType::length(), exec_state)?;
36 let projection: Option<Projection> = args.get_kw_arg_opt("projection", &RuntimeType::any(), exec_state)?;
37
38 let view = CameraView::oriented(
39 orientation,
40 target.map(millimeter_point),
41 distance.map(millimeter_length),
42 projection,
43 vec![args.source_range.into()],
44 )
45 .map_err(|err| view_error(err, &args))?;
46 Ok(KclValue::CameraView { value: Box::new(view) })
47}
48
49pub async fn directed(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
51 let direction: [TyF64; 3] = args.get_unlabeled_kw_arg("direction", &RuntimeType::point3d(), exec_state)?;
58 let up: Option<[TyF64; 3]> = args.get_kw_arg_opt("up", &RuntimeType::point3d(), exec_state)?;
59 let target: Option<[TyF64; 3]> = args.get_kw_arg_opt("target", &RuntimeType::point3d(), exec_state)?;
60 let distance: Option<TyF64> = args.get_kw_arg_opt("distance", &RuntimeType::length(), exec_state)?;
61 let projection: Option<Projection> = args.get_kw_arg_opt("projection", &RuntimeType::any(), exec_state)?;
62
63 let view = CameraView::directed(
64 unitless_direction(direction),
65 up.map(unitless_direction),
66 target.map(millimeter_point),
67 distance.map(millimeter_length),
68 projection,
69 vec![args.source_range.into()],
70 )
71 .map_err(|err| view_error(err, &args))?;
72 Ok(KclValue::CameraView { value: Box::new(view) })
73}
74
75pub async fn named(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
77 let name: String = args.get_unlabeled_kw_arg("name", &RuntimeType::string(), exec_state)?;
80 let camera: CameraView = args.get_kw_arg("camera", &RuntimeType::any(), exec_state)?;
81 let baseline: Visibility = args.get_kw_arg("baseline", &RuntimeType::any(), exec_state)?;
82 let except: Option<Vec<KclValue>> = args.get_kw_arg_opt("except", &RuntimeType::any(), exec_state)?;
83
84 let except_ids = except
85 .as_ref()
86 .map(|objects| except_artifact_ids(objects, &args))
87 .transpose()?;
88
89 let artifact_id = exec_state.next_artifact_id();
92 let view = NamedViewValue::new(
93 artifact_id,
94 name,
95 camera,
96 baseline,
97 except_ids,
98 args.source_range.module_id(),
99 exec_state.registered_named_views(),
100 vec![args.source_range.into()],
101 )
102 .map_err(|err| view_error(err, &args))?;
103 exec_state.add_artifact(Artifact::NamedView(named_view_artifact(
106 &view,
107 CodeRef::placeholder(args.source_range),
108 )));
109
110 Ok(KclValue::NamedView { value: Box::new(view) })
111}
112
113fn except_artifact_ids(objects: &[KclValue], args: &Args) -> Result<Vec<ArtifactId>, KclError> {
122 objects
123 .iter()
124 .map(|object| match object {
125 KclValue::Solid { value } => Ok(value.artifact_id),
126 KclValue::Sketch { value } => Ok(value.artifact_id),
127 KclValue::GdtAnnotation { value } => Ok(ArtifactId::new(value.id)),
128 other => Err(KclError::new_internal(KclErrorDetails::new(
129 format!(
130 "`except` cannot hold {}; the declared signature should have rejected it",
131 other.human_friendly_type()
132 ),
133 vec![args.source_range],
134 ))),
135 })
136 .collect()
137}
138
139fn view_error<E: std::fmt::Display>(err: E, args: &Args) -> KclError {
147 KclError::new_semantic(KclErrorDetails::new(err.to_string(), vec![args.source_range]))
148}
149
150fn millimeter_point([x, y, z]: [TyF64; 3]) -> Point3d {
168 Point3d {
169 x: x.to_mm(),
170 y: y.to_mm(),
171 z: z.to_mm(),
172 units: Some(UnitLength::Millimeters),
173 }
174}
175
176fn millimeter_length(length: TyF64) -> TyF64 {
178 TyF64::new(length.to_mm(), NumericType::mm())
179}
180
181fn unitless_direction([x, y, z]: [TyF64; 3]) -> Point3d {
188 Point3d {
189 x: x.to_mm(),
190 y: y.to_mm(),
191 z: z.to_mm(),
192 units: None,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use crate::execution::ArtifactId;
199 use crate::execution::KclValue;
200 use crate::execution::Visibility;
201 use crate::execution::parse_execute;
202
203 const TWO_SOLIDS: &str = r#"@settings(experimentalFeatures = allow)
208
209plateSketch = sketch(on = XY) {
210 edge1 = line(start = [var 0mm, var 0mm], end = [var 40mm, var 0mm])
211 edge2 = line(start = [var 40mm, var 0mm], end = [var 40mm, var 20mm])
212 edge3 = line(start = [var 40mm, var 20mm], end = [var 0mm, var 20mm])
213 edge4 = line(start = [var 0mm, var 20mm], end = [var 0mm, var 0mm])
214 coincident([edge1.end, edge2.start])
215 coincident([edge2.end, edge3.start])
216 coincident([edge3.end, edge4.start])
217 coincident([edge4.end, edge1.start])
218}
219plateRegion = region(point = [20mm, 10mm], sketch = plateSketch)
220plate = extrude(plateRegion, length = 5mm)
221
222bossSketch = sketch(on = XY) {
223 edge1 = line(start = [var 50mm, var 0mm], end = [var 60mm, var 0mm])
224 edge2 = line(start = [var 60mm, var 0mm], end = [var 60mm, var 10mm])
225 edge3 = line(start = [var 60mm, var 10mm], end = [var 50mm, var 10mm])
226 edge4 = line(start = [var 50mm, var 10mm], end = [var 50mm, var 0mm])
227 coincident([edge1.end, edge2.start])
228 coincident([edge2.end, edge3.start])
229 coincident([edge3.end, edge4.start])
230 coincident([edge4.end, edge1.start])
231}
232bossRegion = region(point = [55mm, 5mm], sketch = bossSketch)
233boss = extrude(bossRegion, length = 8mm)
234"#;
235
236 #[tokio::test(flavor = "multi_thread")]
239 async fn named_shows_everything_under_a_show_baseline() {
240 let program = format!(
241 "{TWO_SOLIDS}\nv = view::named(\n \"Overview\",\n camera = view::oriented(view::Orientation::Isometric),\n baseline = view::Visibility::Show,\n)\n"
242 );
243 let result = parse_execute(&program).await.expect("the program executes");
244
245 let KclValue::NamedView { value } = result.variable("v") else {
246 panic!("`v` is not a named view");
247 };
248 assert_eq!(value.name(), "Overview");
249 assert_eq!(value.baseline(), Visibility::Show);
250 assert!(value.except_ids().is_empty());
251 }
252
253 #[tokio::test(flavor = "multi_thread")]
257 async fn named_excepts_the_objects_it_is_given() {
258 let program = format!(
259 "{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"
260 );
261 let result = parse_execute(&program).await.expect("the program executes");
262
263 let KclValue::Solid { value: plate } = result.variable("plate") else {
264 panic!("`plate` is not a solid");
265 };
266 let KclValue::Solid { value: boss } = result.variable("boss") else {
267 panic!("`boss` is not a solid");
268 };
269 let KclValue::NamedView { value } = result.variable("v") else {
270 panic!("`v` is not a named view");
271 };
272
273 assert_eq!(value.baseline(), Visibility::Hide);
274 assert_eq!(value.except_ids().to_vec(), vec![plate.artifact_id, boss.artifact_id]);
276 }
277
278 #[tokio::test(flavor = "multi_thread")]
283 async fn named_excepts_more_than_one_kind() {
284 let program = format!(
285 "{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"
286 );
287 let result = parse_execute(&program).await.expect("the program executes");
288
289 let KclValue::Solid { value: plate } = result.variable("plate") else {
290 panic!("`plate` is not a solid");
291 };
292 let KclValue::Sketch { value: sketch } = result.variable("bossRegion") else {
296 panic!("`bossRegion` is not a sketch");
297 };
298 let KclValue::GdtAnnotation { value: note } = result.variable("note") else {
299 panic!("`note` is not an annotation");
300 };
301 let KclValue::NamedView { value } = result.variable("v") else {
302 panic!("`v` is not a named view");
303 };
304
305 assert_eq!(
306 value.except_ids().to_vec(),
307 vec![plate.artifact_id, sketch.artifact_id, ArtifactId::new(note.id)]
308 );
309 }
310
311 async fn execution_error(code: &str) -> String {
321 let program = format!("@settings(experimentalFeatures = allow)\n{code}");
322 match parse_execute(&program).await {
323 Ok(_) => panic!("expected `{code}` to be rejected, but it executed"),
324 Err(err) => err.message().to_owned(),
325 }
326 }
327
328 async fn issues_without_opt_in(code: &str) -> Vec<String> {
333 let result = parse_execute(code).await.expect("experimental use is not fatal");
334 result.issues().iter().map(|issue| issue.message.clone()).collect()
335 }
336
337 #[tokio::test(flavor = "multi_thread")]
340 async fn rejected_arguments_report_their_reason() {
341 assert_eq!(
343 execution_error("v = view::directed([1 / 0, 0, 0])").await,
344 "`direction` must have finite coordinates."
345 );
346 assert_eq!(
347 execution_error("v = view::directed([0, 1, 0], up = [0, 0, 1 / 0])").await,
348 "`up` must have finite coordinates."
349 );
350 assert_eq!(
351 execution_error("v = view::directed([0, 1, 0], target = [1 / 0, 0, 0])").await,
352 "`target` must have finite coordinates."
353 );
354 assert_eq!(
355 execution_error("v = view::oriented(view::Orientation::Front, target = [0, 1 / 0, 0])").await,
356 "`target` must have finite coordinates."
357 );
358 assert_eq!(
359 execution_error("v = view::oriented(view::Orientation::Front, distance = 1 / 0)").await,
360 "`distance` must be a finite number."
361 );
362 assert_eq!(
363 execution_error("v = view::directed([0, 1, 0], distance = 0)").await,
364 "`distance` must be greater than zero."
365 );
366 assert_eq!(
367 execution_error("v = view::oriented(view::Orientation::Front, distance = -50)").await,
368 "`distance` must be greater than zero."
369 );
370 assert_eq!(
371 execution_error("v = view::directed([0, 0, 0])").await,
372 "`direction` must be a non-zero vector."
373 );
374 assert_eq!(
375 execution_error("v = view::directed([0, 1, 0], up = [0, 0, 0])").await,
376 "`up` must be a non-zero vector."
377 );
378 assert_eq!(
379 execution_error("v = view::directed([0, 0, 1])").await,
380 "`direction` and `up` must not be parallel or nearly parallel."
381 );
382 }
383
384 #[tokio::test(flavor = "multi_thread")]
389 async fn named_rejects_names_it_cannot_identify_a_view_by() {
390 let showing = "camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show";
392
393 assert_eq!(
394 execution_error(&format!(r#"v = view::named("", {showing})"#)).await,
395 "A view's name must not be empty."
396 );
397 assert_eq!(
398 execution_error(&format!(r#"v = view::named(" ", {showing})"#)).await,
399 "A view's name must not be only whitespace."
400 );
401 assert_eq!(
402 execution_error(&format!(r#"v = view::named("Front ", {showing})"#)).await,
403 "A view's name must not start or end with whitespace. Use `string::trim()` to remove it."
404 );
405 assert_eq!(
406 execution_error(&format!(r#"v = view::named(" Front", {showing})"#)).await,
407 "A view's name must not start or end with whitespace. Use `string::trim()` to remove it."
408 );
409 assert_eq!(
410 execution_error(&format!(r#"v = view::named("Default View", {showing})"#)).await,
411 "`Default View` is reserved for the view of the scene generated on successful execution of the program. Please give this view a different name."
412 );
413 assert_eq!(
414 execution_error(&format!(
415 "a = view::named(\"Front\", {showing})\nb = view::named(\"Front\", {showing})"
416 ))
417 .await,
418 "A view named `Front` already exists. Every view needs its own name, and names are compared exactly, so `Front` and `front` are different names."
419 );
420 }
421
422 #[tokio::test(flavor = "multi_thread")]
426 async fn named_accepts_names_that_differ_only_in_case() {
427 let showing = "camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show";
428 let program = format!(
429 "@settings(experimentalFeatures = allow)\na = view::named(\"Front\", {showing})\nb = view::named(\"front\", {showing})\n"
430 );
431 let result = parse_execute(&program).await.expect("the program executes");
432
433 let KclValue::NamedView { value: upper } = result.variable("a") else {
434 panic!("`a` is not a named view");
435 };
436 let KclValue::NamedView { value: lower } = result.variable("b") else {
437 panic!("`b` is not a named view");
438 };
439 assert_eq!(upper.name(), "Front");
440 assert_eq!(lower.name(), "front");
441 }
442
443 #[tokio::test(flavor = "multi_thread")]
448 async fn named_requires_a_baseline() {
449 assert_eq!(
450 execution_error(r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front))"#).await,
451 "The `view::named` function requires a keyword argument `baseline`"
452 );
453 }
454
455 #[tokio::test(flavor = "multi_thread")]
460 async fn named_rejects_wrongly_typed_arguments() {
461 assert_eq!(
463 execution_error(r#"v = view::named("Front", baseline = view::Visibility::Show)"#).await,
464 "The `view::named` function requires a keyword argument `camera`"
465 );
466 assert_eq!(
470 execution_error(r#"v = view::named("Front", camera = "nope", baseline = view::Visibility::Show)"#).await,
471 "camera requires a value with type `CameraView`, but found a value with type `string`."
472 );
473 assert_eq!(
476 execution_error(
477 r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Orientation::Top)"#
478 )
479 .await,
480 "baseline requires a value with type `Visibility`, but found a value of enum `Orientation` (with type `Orientation`)."
481 );
482 assert_eq!(
484 execution_error(
485 r#"v = view::named(view::Visibility::Show, camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show)"#
486 )
487 .await,
488 "The input argument of `view::named` requires a value with type `string`, but found a value of enum `Visibility` (with type `Visibility`)."
489 );
490 }
491
492 #[tokio::test(flavor = "multi_thread")]
497 async fn declaring_a_view_requires_the_experimental_opt_in() {
498 assert!(
499 issues_without_opt_in(
500 r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show)"#
501 )
502 .await
503 .contains(&"Use of `view::named` is experimental and may change or be removed.".to_owned())
504 );
505 }
506
507 #[tokio::test(flavor = "multi_thread")]
514 async fn named_excepts_a_sketch_block_by_its_own_id() {
515 let program = format!(
516 "{TWO_SOLIDS}\nv = view::named(\n \"Block\",\n camera = view::oriented(view::Orientation::Front),\n baseline = view::Visibility::Hide,\n except = [plateSketch],\n)\n"
517 );
518 let result = parse_execute(&program).await.expect("the program executes");
519
520 let KclValue::Sketch { value: region } = result.variable("plateRegion") else {
521 panic!("`plateRegion` is not a sketch");
522 };
523 let KclValue::NamedView { value } = result.variable("v") else {
524 panic!("`v` is not a named view");
525 };
526
527 assert_eq!(value.except_ids().len(), 1);
528 assert_ne!(
529 value.except_ids()[0],
530 region.artifact_id,
531 "the block and the region taken from it are different artifacts"
532 );
533 }
534
535 #[tokio::test(flavor = "multi_thread")]
539 async fn named_rejects_an_empty_except_list() {
540 assert_eq!(
541 execution_error(
542 r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Hide, except = [])"#
543 )
544 .await,
545 "except requires one or more `Solid`s or `Sketch`s or `GdtAnnotation`s (`[Solid | Sketch | GdtAnnotation; 1+]`), but found an empty array (with type `[any; 0]`)."
546 );
547 }
548
549 #[tokio::test(flavor = "multi_thread")]
561 async fn wrongly_typed_arguments_are_rejected() {
562 assert_eq!(
564 execution_error(r#"v = view::oriented("Front")"#).await,
565 "The input argument of `view::oriented` requires a value with type `Orientation`, but found a value with type `string`."
566 );
567 assert_eq!(
570 execution_error("v = view::oriented(view::Projection::Perspective)").await,
571 "The input argument of `view::oriented` requires a value with type `Orientation`, but found a value of enum `Projection` (with type `Projection`)."
572 );
573 assert_eq!(
576 execution_error("type MyOrientation { | Front }\nv = view::oriented(MyOrientation::Front)").await,
577 "The input argument of `view::oriented` requires a value with type `Orientation`, but found a value of enum `MyOrientation` (with type `MyOrientation`)."
578 );
579 assert_eq!(
581 execution_error("v = view::oriented(view::Orientation::Front, projection = view::Orientation::Top)").await,
582 "projection requires a value with type `Projection`, but found a value of enum `Orientation` (with type `Orientation`)."
583 );
584 assert_eq!(
586 execution_error(r#"v = view::directed("nope")"#).await,
587 "The input argument of `view::directed` requires a value with type `Point3d`, but found a value with type `string`."
588 );
589 }
590
591 #[tokio::test(flavor = "multi_thread")]
601 async fn calling_a_constructor_requires_the_experimental_opt_in() {
602 assert!(
603 issues_without_opt_in("v = view::directed([0, 1, -2])")
604 .await
605 .contains(&"Use of `view::directed` is experimental and may change or be removed.".to_owned())
606 );
607
608 assert!(
611 issues_without_opt_in("v = view::oriented(view::Orientation::Front)")
612 .await
613 .contains(&"Use of `view::oriented` is experimental and may change or be removed.".to_owned())
614 );
615 }
616
617 #[tokio::test(flavor = "multi_thread")]
625 async fn opaque_types_resolve_in_signatures() {
626 let code = r#"@settings(experimentalFeatures = allow)
627import CameraView, NamedView from "std::view"
628
629fn acceptsCamera(@camera: CameraView) {
630 return 0
631}
632
633fn passesNamed(@input: NamedView): NamedView {
634 return input
635}
636"#;
637 if let Err(err) = parse_execute(code).await {
638 panic!("expected the declarations to resolve, but got: {}", err.message());
639 }
640 }
641}