use kcl_api::UnitLength;
use crate::SourceRange;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::Artifact;
use crate::execution::ArtifactId;
use crate::execution::CameraView;
use crate::execution::CodeRef;
use crate::execution::ExecState;
use crate::execution::KclValue;
use crate::execution::NamedViewValue;
use crate::execution::Orientation;
use crate::execution::Point3d;
use crate::execution::Projection;
use crate::execution::Visibility;
use crate::execution::named_view_artifact;
use crate::execution::types::NumericType;
use crate::execution::types::NumericTypeExt;
use crate::execution::types::RuntimeType;
use crate::std::Args;
use crate::std::args::TyF64;
pub async fn oriented(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let orientation: Orientation = args.get_unlabeled_kw_arg("orientation", &RuntimeType::any(), exec_state)?;
let target: Option<[TyF64; 3]> = args.get_kw_arg_opt("target", &RuntimeType::point3d(), exec_state)?;
let distance: Option<TyF64> = args.get_kw_arg_opt("distance", &RuntimeType::length(), exec_state)?;
let projection: Option<Projection> = args.get_kw_arg_opt("projection", &RuntimeType::any(), exec_state)?;
let view = CameraView::oriented(
orientation,
target.map(millimeter_point),
distance.map(millimeter_length),
projection,
vec![args.source_range.into()],
)
.map_err(|err| view_error(err, &args))?;
Ok(KclValue::CameraView { value: Box::new(view) })
}
pub async fn directed(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let direction: [TyF64; 3] = args.get_unlabeled_kw_arg("direction", &RuntimeType::point3d(), exec_state)?;
let up: Option<[TyF64; 3]> = args.get_kw_arg_opt("up", &RuntimeType::point3d(), exec_state)?;
let target: Option<[TyF64; 3]> = args.get_kw_arg_opt("target", &RuntimeType::point3d(), exec_state)?;
let distance: Option<TyF64> = args.get_kw_arg_opt("distance", &RuntimeType::length(), exec_state)?;
let projection: Option<Projection> = args.get_kw_arg_opt("projection", &RuntimeType::any(), exec_state)?;
let view = CameraView::directed(
unitless_direction(direction),
up.map(unitless_direction),
target.map(millimeter_point),
distance.map(millimeter_length),
projection,
vec![args.source_range.into()],
)
.map_err(|err| view_error(err, &args))?;
Ok(KclValue::CameraView { value: Box::new(view) })
}
pub async fn named(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let name: String = args.get_unlabeled_kw_arg("name", &RuntimeType::string(), exec_state)?;
let camera: CameraView = args.get_kw_arg("camera", &RuntimeType::any(), exec_state)?;
let baseline: Visibility = args.get_kw_arg("baseline", &RuntimeType::any(), exec_state)?;
let except: Option<Vec<KclValue>> = args.get_kw_arg_opt("except", &RuntimeType::any(), exec_state)?;
let except_ids = except
.as_ref()
.map(|objects| except_artifact_ids(objects, args.source_range))
.transpose()?;
let artifact_id = exec_state.next_artifact_id();
let view = NamedViewValue::new(
artifact_id,
name,
camera,
baseline,
except_ids,
args.source_range.module_id(),
exec_state.registered_named_views(),
vec![args.source_range.into()],
)
.map_err(|err| view_error(err, &args))?;
exec_state.add_artifact(Artifact::NamedView(named_view_artifact(
&view,
CodeRef::placeholder(args.source_range),
)));
Ok(KclValue::NamedView { value: Box::new(view) })
}
fn except_artifact_ids(objects: &[KclValue], source_range: SourceRange) -> Result<Vec<ArtifactId>, KclError> {
objects
.iter()
.map(|object| match object {
KclValue::Solid { value } => Ok(value.artifact_id),
KclValue::Sketch { value } => Ok(value.artifact_id),
KclValue::GdtAnnotation { value } => Ok(ArtifactId::new(value.id)),
KclValue::Helix { value } => Ok(value.artifact_id),
KclValue::Plane { value } => {
if value.is_standard() || value.is_uninitialized() {
Err(KclError::new_semantic(KclErrorDetails::new(
"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()`."
.to_owned(),
vec![source_range],
)))
} else {
Ok(value.artifact_id)
}
}
KclValue::ImportedGeometry(value) => Ok(ArtifactId::new(value.id)),
other => Err(KclError::new_internal(KclErrorDetails::new(
format!(
"`except` cannot hold {}; the declared signature should have rejected it",
other.human_friendly_type()
),
vec![source_range],
))),
})
.collect()
}
fn view_error<E: std::fmt::Display>(err: E, args: &Args) -> KclError {
KclError::new_semantic(KclErrorDetails::new(err.to_string(), vec![args.source_range]))
}
fn millimeter_point([x, y, z]: [TyF64; 3]) -> Point3d {
Point3d {
x: x.to_mm(),
y: y.to_mm(),
z: z.to_mm(),
units: Some(UnitLength::Millimeters),
}
}
fn millimeter_length(length: TyF64) -> TyF64 {
TyF64::new(length.to_mm(), NumericType::mm())
}
fn unitless_direction([x, y, z]: [TyF64; 3]) -> Point3d {
Point3d {
x: x.to_mm(),
y: y.to_mm(),
z: z.to_mm(),
units: None,
}
}
#[cfg(test)]
mod tests {
use crate::execution::ArtifactId;
use crate::execution::KclValue;
use crate::execution::Visibility;
use crate::execution::parse_execute;
const TWO_SOLIDS: &str = r#"@settings(experimentalFeatures = allow)
plateSketch = sketch(on = XY) {
edge1 = line(start = [var 0mm, var 0mm], end = [var 40mm, var 0mm])
edge2 = line(start = [var 40mm, var 0mm], end = [var 40mm, var 20mm])
edge3 = line(start = [var 40mm, var 20mm], end = [var 0mm, var 20mm])
edge4 = line(start = [var 0mm, var 20mm], end = [var 0mm, var 0mm])
coincident([edge1.end, edge2.start])
coincident([edge2.end, edge3.start])
coincident([edge3.end, edge4.start])
coincident([edge4.end, edge1.start])
}
plateRegion = region(point = [20mm, 10mm], sketch = plateSketch)
plate = extrude(plateRegion, length = 5mm)
bossSketch = sketch(on = XY) {
edge1 = line(start = [var 50mm, var 0mm], end = [var 60mm, var 0mm])
edge2 = line(start = [var 60mm, var 0mm], end = [var 60mm, var 10mm])
edge3 = line(start = [var 60mm, var 10mm], end = [var 50mm, var 10mm])
edge4 = line(start = [var 50mm, var 10mm], end = [var 50mm, var 0mm])
coincident([edge1.end, edge2.start])
coincident([edge2.end, edge3.start])
coincident([edge3.end, edge4.start])
coincident([edge4.end, edge1.start])
}
bossRegion = region(point = [55mm, 5mm], sketch = bossSketch)
boss = extrude(bossRegion, length = 8mm)
"#;
#[tokio::test(flavor = "multi_thread")]
async fn named_shows_everything_under_a_show_baseline() {
let program = format!(
"{TWO_SOLIDS}\nv = view::named(\n \"Overview\",\n camera = view::oriented(view::Orientation::Isometric),\n baseline = view::Visibility::Show,\n)\n"
);
let result = parse_execute(&program).await.expect("the program executes");
let KclValue::NamedView { value } = result.variable("v") else {
panic!("`v` is not a named view");
};
assert_eq!(value.name(), "Overview");
assert_eq!(value.baseline(), Visibility::Show);
assert!(value.except_ids().is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn named_excepts_the_objects_it_is_given() {
let program = format!(
"{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"
);
let result = parse_execute(&program).await.expect("the program executes");
let KclValue::Solid { value: plate } = result.variable("plate") else {
panic!("`plate` is not a solid");
};
let KclValue::Solid { value: boss } = result.variable("boss") else {
panic!("`boss` is not a solid");
};
let KclValue::NamedView { value } = result.variable("v") else {
panic!("`v` is not a named view");
};
assert_eq!(value.baseline(), Visibility::Hide);
assert_eq!(value.except_ids().to_vec(), vec![plate.artifact_id, boss.artifact_id]);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_excepts_more_than_one_kind() {
let program = format!(
"{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"
);
let result = parse_execute(&program).await.expect("the program executes");
let KclValue::Solid { value: plate } = result.variable("plate") else {
panic!("`plate` is not a solid");
};
let KclValue::Sketch { value: sketch } = result.variable("bossRegion") else {
panic!("`bossRegion` is not a sketch");
};
let KclValue::GdtAnnotation { value: note } = result.variable("note") else {
panic!("`note` is not an annotation");
};
let KclValue::NamedView { value } = result.variable("v") else {
panic!("`v` is not a named view");
};
assert_eq!(
value.except_ids().to_vec(),
vec![plate.artifact_id, sketch.artifact_id, ArtifactId::new(note.id)]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_excepts_a_custom_plane_and_helix_by_their_artifact_ids() {
let program = r#"@settings(experimentalFeatures = allow)
inspectionPlane = offsetPlane(XY, offset = 20mm)
spring = helix(
axis = Z,
radius = 5mm,
length = 20mm,
revolutions = 4,
angleStart = 0deg,
)
v = view::named(
"Construction geometry",
camera = view::oriented(view::Orientation::Isometric),
baseline = view::Visibility::Hide,
except = [inspectionPlane, spring],
)
"#;
let result = parse_execute(program).await.expect("the program executes");
let KclValue::Plane { value: plane } = result.variable("inspectionPlane") else {
panic!("`inspectionPlane` is not a plane");
};
let KclValue::Helix { value: helix } = result.variable("spring") else {
panic!("`spring` is not a helix");
};
let KclValue::NamedView { value } = result.variable("v") else {
panic!("`v` is not a named view");
};
assert!(plane.is_initialized());
assert_eq!(value.except_ids().to_vec(), vec![plane.artifact_id, helix.artifact_id]);
}
#[test]
fn named_excepts_imported_geometry_by_its_artifact_id() {
let id = uuid::Uuid::from_u128(1);
let imported = KclValue::ImportedGeometry(crate::execution::ImportedGeometry::new(
id,
vec!["part.step".to_owned()],
Vec::new(),
));
assert_eq!(
super::except_artifact_ids(&[imported], crate::SourceRange::default()).expect("the value is accepted"),
vec![ArtifactId::new(id)]
);
}
async fn execution_error(code: &str) -> String {
let program = format!("@settings(experimentalFeatures = allow)\n{code}");
match parse_execute(&program).await {
Ok(_) => panic!("expected `{code}` to be rejected, but it executed"),
Err(err) => err.message().to_owned(),
}
}
async fn issues_without_opt_in(code: &str) -> Vec<String> {
let result = parse_execute(code).await.expect("experimental use is not fatal");
result.issues().iter().map(|issue| issue.message.clone()).collect()
}
#[tokio::test(flavor = "multi_thread")]
async fn rejected_arguments_report_their_reason() {
assert_eq!(
execution_error("v = view::directed([1 / 0, 0, 0])").await,
"`direction` must have finite coordinates."
);
assert_eq!(
execution_error("v = view::directed([0, 1, 0], up = [0, 0, 1 / 0])").await,
"`up` must have finite coordinates."
);
assert_eq!(
execution_error("v = view::directed([0, 1, 0], target = [1 / 0, 0, 0])").await,
"`target` must have finite coordinates."
);
assert_eq!(
execution_error("v = view::oriented(view::Orientation::Front, target = [0, 1 / 0, 0])").await,
"`target` must have finite coordinates."
);
assert_eq!(
execution_error("v = view::oriented(view::Orientation::Front, distance = 1 / 0)").await,
"`distance` must be a finite number."
);
assert_eq!(
execution_error("v = view::directed([0, 1, 0], distance = 0)").await,
"`distance` must be greater than zero."
);
assert_eq!(
execution_error("v = view::oriented(view::Orientation::Front, distance = -50)").await,
"`distance` must be greater than zero."
);
assert_eq!(
execution_error("v = view::directed([0, 0, 0])").await,
"`direction` must be a non-zero vector."
);
assert_eq!(
execution_error("v = view::directed([0, 1, 0], up = [0, 0, 0])").await,
"`up` must be a non-zero vector."
);
assert_eq!(
execution_error("v = view::directed([0, 0, 1])").await,
"`direction` and `up` must not be parallel or nearly parallel."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_rejects_names_it_cannot_identify_a_view_by() {
let showing = "camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show";
assert_eq!(
execution_error(&format!(r#"v = view::named("", {showing})"#)).await,
"A view's name must not be empty."
);
assert_eq!(
execution_error(&format!(r#"v = view::named(" ", {showing})"#)).await,
"A view's name must not be only whitespace."
);
assert_eq!(
execution_error(&format!(r#"v = view::named("Front ", {showing})"#)).await,
"A view's name must not start or end with whitespace. Use `string::trim()` to remove it."
);
assert_eq!(
execution_error(&format!(r#"v = view::named(" Front", {showing})"#)).await,
"A view's name must not start or end with whitespace. Use `string::trim()` to remove it."
);
assert_eq!(
execution_error(&format!(r#"v = view::named("Default View", {showing})"#)).await,
"`Default View` is reserved for the view of the scene generated on successful execution of the program. Please give this view a different name."
);
assert_eq!(
execution_error(&format!(
"a = view::named(\"Front\", {showing})\nb = view::named(\"Front\", {showing})"
))
.await,
"A view named `Front` already exists. Every view needs its own name, and names are compared exactly, so `Front` and `front` are different names."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_accepts_names_that_differ_only_in_case() {
let showing = "camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show";
let program = format!(
"@settings(experimentalFeatures = allow)\na = view::named(\"Front\", {showing})\nb = view::named(\"front\", {showing})\n"
);
let result = parse_execute(&program).await.expect("the program executes");
let KclValue::NamedView { value: upper } = result.variable("a") else {
panic!("`a` is not a named view");
};
let KclValue::NamedView { value: lower } = result.variable("b") else {
panic!("`b` is not a named view");
};
assert_eq!(upper.name(), "Front");
assert_eq!(lower.name(), "front");
}
#[tokio::test(flavor = "multi_thread")]
async fn named_requires_a_baseline() {
assert_eq!(
execution_error(r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front))"#).await,
"The `view::named` function requires a keyword argument `baseline`"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_rejects_wrongly_typed_arguments() {
assert_eq!(
execution_error(r#"v = view::named("Front", baseline = view::Visibility::Show)"#).await,
"The `view::named` function requires a keyword argument `camera`"
);
assert_eq!(
execution_error(r#"v = view::named("Front", camera = "nope", baseline = view::Visibility::Show)"#).await,
"camera requires a value with type `CameraView`, but found a value with type `string`."
);
assert_eq!(
execution_error(
r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Orientation::Top)"#
)
.await,
"baseline requires a value with type `Visibility`, but found a value of enum `Orientation` (with type `Orientation`)."
);
assert_eq!(
execution_error(
r#"v = view::named(view::Visibility::Show, camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show)"#
)
.await,
"The input argument of `view::named` requires a value with type `string`, but found a value of enum `Visibility` (with type `Visibility`)."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn declaring_a_view_requires_the_experimental_opt_in() {
assert!(
issues_without_opt_in(
r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Show)"#
)
.await
.contains(&"Use of `view::named` is experimental and may change or be removed.".to_owned())
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_excepts_a_sketch_block_by_its_own_id() {
let program = format!(
"{TWO_SOLIDS}\nv = view::named(\n \"Block\",\n camera = view::oriented(view::Orientation::Front),\n baseline = view::Visibility::Hide,\n except = [plateSketch],\n)\n"
);
let result = parse_execute(&program).await.expect("the program executes");
let KclValue::Sketch { value: region } = result.variable("plateRegion") else {
panic!("`plateRegion` is not a sketch");
};
let KclValue::NamedView { value } = result.variable("v") else {
panic!("`v` is not a named view");
};
assert_eq!(value.except_ids().len(), 1);
assert_ne!(
value.except_ids()[0],
region.artifact_id,
"the block and the region taken from it are different artifacts"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_rejects_an_empty_except_list() {
assert_eq!(
execution_error(
r#"v = view::named("Front", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Hide, except = [])"#
)
.await,
"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]`)."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn named_rejects_every_default_plane() {
for plane in ["XY", "XZ", "YZ", "-XY", "-XZ", "-YZ"] {
assert_eq!(
execution_error(&format!(
"v = view::named(\"Front\", camera = view::oriented(view::Orientation::Front), baseline = view::Visibility::Hide, except = [{plane}])"
))
.await,
"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()`."
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn named_rejects_an_uninitialized_custom_plane() {
assert_eq!(
execution_error(
r#"customPlane = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
}
v = view::named(
"Front",
camera = view::oriented(view::Orientation::Front),
baseline = view::Visibility::Hide,
except = [customPlane],
)"#
)
.await,
"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()`."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn wrongly_typed_arguments_are_rejected() {
assert_eq!(
execution_error(r#"v = view::oriented("Front")"#).await,
"The input argument of `view::oriented` requires a value with type `Orientation`, but found a value with type `string`."
);
assert_eq!(
execution_error("v = view::oriented(view::Projection::Perspective)").await,
"The input argument of `view::oriented` requires a value with type `Orientation`, but found a value of enum `Projection` (with type `Projection`)."
);
assert_eq!(
execution_error("type MyOrientation { | Front }\nv = view::oriented(MyOrientation::Front)").await,
"The input argument of `view::oriented` requires a value with type `Orientation`, but found a value of enum `MyOrientation` (with type `MyOrientation`)."
);
assert_eq!(
execution_error("v = view::oriented(view::Orientation::Front, projection = view::Orientation::Top)").await,
"projection requires a value with type `Projection`, but found a value of enum `Orientation` (with type `Orientation`)."
);
assert_eq!(
execution_error(r#"v = view::directed("nope")"#).await,
"The input argument of `view::directed` requires a value with type `Point3d`, but found a value with type `string`."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn calling_a_constructor_requires_the_experimental_opt_in() {
assert!(
issues_without_opt_in("v = view::directed([0, 1, -2])")
.await
.contains(&"Use of `view::directed` is experimental and may change or be removed.".to_owned())
);
assert!(
issues_without_opt_in("v = view::oriented(view::Orientation::Front)")
.await
.contains(&"Use of `view::oriented` is experimental and may change or be removed.".to_owned())
);
}
#[tokio::test(flavor = "multi_thread")]
async fn opaque_types_resolve_in_signatures() {
let code = r#"@settings(experimentalFeatures = allow)
import CameraView, NamedView from "std::view"
fn acceptsCamera(@camera: CameraView) {
return 0
}
fn passesNamed(@input: NamedView): NamedView {
return input
}
"#;
if let Err(err) = parse_execute(code).await {
panic!("expected the declarations to resolve, but got: {}", err.message());
}
}
#[tokio::test(flavor = "multi_thread")]
async fn qualified_type_path_resolves_in_signature() {
let code = r#"@settings(experimentalFeatures = allow)
fn passesOrientation(@input: view::Orientation): view::Orientation {
return input
}
"#;
if let Err(err) = parse_execute(code).await {
panic!("expected the declarations to resolve, but got: {}", err.message());
}
}
}