Skip to main content

kcl_lib/std/
view.rs

1//! Standard library functions for named views.
2
3use 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
25/// Create a camera view that looks at the model from a standard orientation.
26pub async fn oriented(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
27    // The declared KCL signature has already coerced every argument, so the
28    // runtime types passed here only convert, they do not validate. Enum
29    // coercion is nominal, so an `any` runtime type still cannot let a
30    // different enum through; `wrongly_typed_arguments_are_rejected` pins
31    // that, because the fallback if it stopped holding is an internal error
32    // rather than a diagnostic the author can act on.
33    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
49/// Create a camera view that looks along a custom direction.
50pub async fn directed(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
51    // The declared KCL signature has already coerced every argument, so the
52    // runtime types passed here only convert, they do not validate. Enum
53    // coercion is nominal, so an `any` runtime type still cannot let a
54    // different enum through; `wrongly_typed_arguments_are_rejected` pins
55    // that, because the fallback if it stopped holding is an internal error
56    // rather than a diagnostic the author can act on.
57    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
75/// Create a named view: a camera paired with the objects it shows or hides.
76pub async fn named(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
77    // As in the camera constructors, the declared KCL signature has already
78    // coerced every argument, so the runtime types here only convert.
79    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    // The id is taken before the existing views are read, because taking one
90    // borrows `exec_state` mutably and reading them borrows it immutably.
91    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    // A view sends no engine command: it is data for a consumer to activate
104    // later, so registering the artifact is the whole effect of the call.
105    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
113/// Reads the artifact id of each object in an `except` list.
114///
115/// The three accepted kinds carry their artifact id differently: a solid and a
116/// sketch each have an `artifact_id` field distinct from their engine id, while
117/// a GD&T annotation has one id used for both, which `gdt::datum` registers as
118/// `ArtifactId::new(annotation.id)`. Any other kind of value means coercion
119/// against the declared signature did not do its job, which is an internal
120/// error rather than something the author can act on.
121fn 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
139/// Reports a rejected argument at the source range of the call that supplied
140/// it. The error's `Display` text is the whole message; the range is what tells
141/// the author which call to look at.
142///
143/// Both `CameraViewError` and `NamedViewError` are written so that their
144/// `Display` text is the author-facing message, which is why one helper serves
145/// every function in this module.
146fn 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
150// Every length a `CameraView` stores is converted to millimeters here, at the
151// boundary where the author's units are still known. A stored view is read by
152// more than one consumer -- the modeling app and STEP export -- and some of
153// those values become engine commands, which are in millimeters. Storing one
154// canonical unit means no consumer has to share a conversion convention with
155// the others; a consumer that reads the number and ignores the unit tag still
156// gets the right camera.
157//
158// These conversions are written here rather than through the shared
159// `FromKclValue for Point3d` impl (`std/args.rs`), which is deliberate. That
160// impl reconciles the three coordinate types with `NumericType::combine_eq_array`
161// and keeps the values as written, so a genuinely mixed-unit point such as
162// `[1inch, 25.4mm, 0]` is stored as the numbers 1, 25.4, 0 with no units at
163// all -- a different point from the one the author wrote. Converting each
164// coordinate individually, as below, is correct for that case.
165
166/// Converts a coerced point argument to a point in millimeters.
167fn 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
176/// Converts a coerced length argument to a length in millimeters.
177fn millimeter_length(length: TyF64) -> TyF64 {
178    TyF64::new(length.to_mm(), NumericType::mm())
179}
180
181/// Converts a coerced point argument to a unitless direction vector. Each
182/// coordinate is read in millimeters first, so the coordinates of a
183/// mixed-unit vector such as `[1inch, 25.4mm, 0]` are on a common scale and
184/// the vector points where the author wrote it. The magnitude is discarded by
185/// normalization in the constructor, so the choice of millimeters here only
186/// has to be consistent across the three coordinates.
187fn 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    /// A sketch V2 program that leaves two solids bound, `plate` and `boss`,
204    /// for a view to except. Mock execution is enough: a view sends no engine
205    /// command, and the artifact ids these solids carry are assigned during
206    /// execution rather than by the engine.
207    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    /// A `Show` baseline with no exception list keeps every object visible. The
237    /// baseline is required, so this is the shortest view a file can declare.
238    #[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    /// The `except` list accepts more than one kind of object in one call, and
254    /// the artifact ids it stores are those of the objects named, in the order
255    /// written.
256    #[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        // The repeated `plate` is dropped, keeping the first occurrence.
275        assert_eq!(value.except_ids().to_vec(), vec![plate.artifact_id, boss.artifact_id]);
276    }
277
278    /// One `except` list may name objects of different kinds, which the
279    /// declared element type `Solid | Sketch | GdtAnnotation` allows. The three
280    /// kinds carry their artifact id differently, so each is read by its own
281    /// arm and this is what pins all three arms at once.
282    #[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        // A region is the sketch value the extrude consumed; the sketch-block
293        // variable itself is an object that coercion converts to a `Sketch`,
294        // so it is the region that carries the artifact id to compare against.
295        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    /// Runs `code` with the experimental opt-in these functions require, and
312    /// returns the message it fails with. Panics if the program succeeds.
313    ///
314    /// These cases run against the mock engine rather than as simulation
315    /// tests: rejected arguments never reach the engine
316    /// Two simulation tests remain, one per constructor
317    /// (`named_views_directed_zero_direction` and
318    /// `named_views_negative_distance`), which pin the rendered diagnostic
319    /// with its source range.
320    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    /// Runs `code` WITHOUT the experimental opt-in and returns the diagnostics
329    /// it reports. Experimental use is recorded as a non-fatal issue rather
330    /// than a returned error, so the program still executes and the issue list
331    /// is the only place the gate is visible.
332    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    /// Every rejected argument reports which argument to change. Each case
338    /// pairs the offending call with the message the author sees.
339    #[tokio::test(flavor = "multi_thread")]
340    async fn rejected_arguments_report_their_reason() {
341        // `1 / 0` is how a KCL program reaches a non-finite value.
342        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    /// Every name a view cannot be identified by reports the message the author
385    /// sees. The rules themselves are pinned in `execution::named_views`, where
386    /// they need no executor; these cases pin that `view::named` reports them
387    /// rather than accepting the name or failing some other way.
388    #[tokio::test(flavor = "multi_thread")]
389    async fn named_rejects_names_it_cannot_identify_a_view_by() {
390        // The two required arguments, so each case below varies only the name.
391        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    /// Two views in one file may differ only in case or spacing, because the
423    /// uniqueness rule compares names exactly. This is the accepting half of
424    /// the duplicate case above.
425    #[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    /// Omitting `baseline` is rejected by the declared signature, before `named`
444    /// runs. It is required so that a view states what it shows rather than
445    /// leaving a reader to know a default, and this pins that the signature is
446    /// what enforces it.
447    #[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    /// `named`'s required arguments and their types are enforced by the declared
456    /// signature. The camera and baseline cases are here for the reason the
457    /// constructors have their own: the alternative to a signature rejection is
458    /// an internal error from `FromKclValue`, which tells the author nothing.
459    #[tokio::test(flavor = "multi_thread")]
460    async fn named_rejects_wrongly_typed_arguments() {
461        // An omitted required argument.
462        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        // A value that is not a camera at all. The wrong value is a string
467        // rather than a number, because a number additionally draws the
468        // incomplete-units hint, which has nothing to do with this rejection.
469        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        // A different enum where `baseline` is declared, which nominal coercion
474        // rejects even though the Rust body reads it with an `any` runtime type.
475        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        // A name that is not a string.
483        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    /// Declaring a view without the experimental opt-in is reported, as calling
493    /// either camera constructor is. `named` needs its own case: the gate is a
494    /// per-function annotation, so covering the constructors says nothing about
495    /// this function.
496    #[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    /// A sketch-block variable is accepted in `except`, and this pins which id it
508    /// contributes. A block and the region taken from it are different artifacts:
509    /// the block stores the path artifact of the whole `sketch(on = ...) { ... }`
510    /// expression, the region its own. Both are nodes a consumer can resolve, so
511    /// the two are a choice the author makes rather than one being wrong. The sim
512    /// test `named_views_except_a_sketch_block` shows the node the block names.
513    #[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    /// An empty `except` list is rejected by the declared element count `1+`,
536    /// before `named` runs. Nothing in the Rust body depends on that, but a list
537    /// that excepts nothing says nothing, so this pins where it is caught.
538    #[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    /// An argument of the wrong type is rejected by the declared signature,
550    /// before either function runs.
551    ///
552    /// This is what lets the implementations read `orientation` and
553    /// `projection` with an `any` runtime type: enum coercion is nominal, so
554    /// the signature admits only the enum it names. Were that to stop
555    /// holding, the value would reach `FromKclValue` and fail there, and the
556    /// author would get "Mismatch between type coercion and value extraction
557    /// (this isn't your fault)" from `std/args.rs` instead of a diagnostic
558    /// naming the argument. The point of these cases is that the good message
559    /// is the one that appears.
560    #[tokio::test(flavor = "multi_thread")]
561    async fn wrongly_typed_arguments_are_rejected() {
562        // A value of an unrelated type.
563        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        // A different enum from the same module. Nominal coercion rejects it
568        // even though `any` was requested.
569        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        // A user-defined enum declaring a variant of the same name. Coercion
574        // compares the declaring type, not the variant spelling.
575        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        // A labeled argument, which reports in its own wording.
580        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        // The same protection covers the non-enum arguments.
585        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    /// Calling either constructor without the experimental opt-in is
592    /// reported, whether or not the call mentions an enum.
593    ///
594    /// The sim test `named_views_module_requires_opt_in` covers the other
595    /// half of the gate, a bare enum variant. This covers the functions
596    /// themselves: `view::directed` with a plain vector names no enum, so the
597    /// only thing gating it is its own `@(experimental = true)`. Named views
598    /// stay unreleasable until enums stabilise precisely because a consumer
599    /// must opt in, so the gate silently lapsing is the failure to catch.
600    #[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        // This call also uses an enum variant, so it reports both halves of
609        // the gate; the function's own diagnostic is the one asserted here.
610        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    /// The opaque `std::view` types resolve where a signature names them.
618    /// Resolution happens when the declaration executes, so executing these
619    /// declarations is the whole assertion; neither function is called.
620    ///
621    /// Type annotations parse only as bare identifiers, so the namespaced
622    /// spelling `view::CameraView` cannot appear in a signature and an
623    /// explicit import is the only route to these types from user code.
624    #[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}