Options and docs for foreign imports (#5351)
* Annotations for imports of foreign files Signed-off-by: Nick Cameron <nrc@ncameron.org> * Document foreign imports Signed-off-by: Nick Cameron <nrc@ncameron.org> --------- Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
@ -57,3 +57,55 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
|
|||||||
```
|
```
|
||||||
import increment as inc, decrement as dec from "util.kcl"
|
import increment as inc, decrement as dec from "util.kcl"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Importing files from other CAD systems
|
||||||
|
|
||||||
|
`import` can also be used to import files from other CAD systems. The format of the statement is the
|
||||||
|
same as for KCL files. You can only import the whole file, not items from it. E.g.,
|
||||||
|
|
||||||
|
```
|
||||||
|
import "tests/inputs/cube.obj"
|
||||||
|
|
||||||
|
// Use `cube` just like a KCL object.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
import "tests/inputs/cube-2.sldprt" as cube
|
||||||
|
|
||||||
|
// Use `cube` just like a KCL object.
|
||||||
|
```
|
||||||
|
|
||||||
|
You can make the file format explicit using a format attribute (useful if using a different
|
||||||
|
extension), e.g.,
|
||||||
|
|
||||||
|
```
|
||||||
|
@(format = obj)
|
||||||
|
import "tests/inputs/cube"
|
||||||
|
```
|
||||||
|
|
||||||
|
For formats lacking unit data (such as STL, OBJ, or PLY files), the default
|
||||||
|
unit of measurement is millimeters. Alternatively you may specify the unit
|
||||||
|
by using an attirbute. Likewise, you can also specify a coordinate system. E.g.,
|
||||||
|
|
||||||
|
```
|
||||||
|
@(unitLength = ft, coords = opengl)
|
||||||
|
import "tests/inputs/cube.obj"
|
||||||
|
```
|
||||||
|
|
||||||
|
When importing a GLTF file, the bin file will be imported as well.
|
||||||
|
|
||||||
|
Import paths are relative to the current project directory. Imports currently only work when
|
||||||
|
using the native Modeling App, not in the browser.
|
||||||
|
|
||||||
|
### Supported values
|
||||||
|
|
||||||
|
File formats: `fbx`, `gltf`/`glb`, `obj`+, `ply`+, `sldprt`, `step`/`stp`, `stl`+. (Those marked with a
|
||||||
|
'+' support customising the length unit and coordinate system).
|
||||||
|
|
||||||
|
Length units: `mm` (the default), `cm`, `m`, `inch`, `ft`, `yd`.
|
||||||
|
|
||||||
|
Coordinate systems:
|
||||||
|
|
||||||
|
- `zoo` (the default), forward: -Y, up: +Z, handedness: right
|
||||||
|
- `opengl`, forward: +Z, up: +Y, handedness: right
|
||||||
|
- `vulkan`, forward: +Z, up: -Y, handedness: left
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
//! Data on available annotations.
|
//! Data on available annotations.
|
||||||
|
|
||||||
use super::kcl_value::{UnitAngle, UnitLen};
|
use kittycad_modeling_cmds::coord::{System, KITTYCAD, OPENGL, VULKAN};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::KclErrorDetails,
|
errors::KclErrorDetails,
|
||||||
|
execution::kcl_value::{UnitAngle, UnitLen},
|
||||||
parsing::ast::types::{Expr, Node, NonCodeValue, ObjectProperty},
|
parsing::ast::types::{Expr, Node, NonCodeValue, ObjectProperty},
|
||||||
KclError, SourceRange,
|
KclError, SourceRange,
|
||||||
};
|
};
|
||||||
@ -11,6 +13,12 @@ pub(crate) const SETTINGS: &str = "settings";
|
|||||||
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
|
||||||
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
|
||||||
pub(super) const NO_PRELUDE: &str = "no_prelude";
|
pub(super) const NO_PRELUDE: &str = "no_prelude";
|
||||||
|
pub(super) const IMPORT_FORMAT: &str = "format";
|
||||||
|
pub(super) const IMPORT_FORMAT_VALUES: [&str; 9] = ["fbx", "gltf", "glb", "obj", "ply", "sldprt", "stp", "step", "stl"];
|
||||||
|
pub(super) const IMPORT_COORDS: &str = "coords";
|
||||||
|
pub(super) const IMPORT_COORDS_VALUES: [(&str, &System); 3] =
|
||||||
|
[("zoo", KITTYCAD), ("opengl", OPENGL), ("vulkan", VULKAN)];
|
||||||
|
pub(super) const IMPORT_LENGTH_UNIT: &str = "lengthUnit";
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub(super) enum AnnotationScope {
|
pub(super) enum AnnotationScope {
|
||||||
@ -36,6 +44,18 @@ pub(super) fn expect_properties<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn unnamed_properties<'a>(
|
||||||
|
annotations: impl Iterator<Item = &'a NonCodeValue>,
|
||||||
|
) -> Option<&'a [Node<ObjectProperty>]> {
|
||||||
|
for annotation in annotations {
|
||||||
|
if let NonCodeValue::Annotation { name: None, properties } = annotation {
|
||||||
|
return properties.as_deref();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
|
pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Identifier(id) => Ok(&id.name),
|
Expr::Identifier(id) => Ok(&id.name),
|
||||||
@ -57,7 +77,7 @@ impl UnitLen {
|
|||||||
"yd" => Ok(UnitLen::Yards),
|
"yd" => Ok(UnitLen::Yards),
|
||||||
value => Err(KclError::Semantic(KclErrorDetails {
|
value => Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unexpected settings value: `{value}`; expected one of `mm`, `cm`, `m`, `inch`, `ft`, `yd`"
|
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `inch`, `ft`, `yd`"
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
})),
|
})),
|
||||||
@ -71,7 +91,7 @@ impl UnitAngle {
|
|||||||
"deg" => Ok(UnitAngle::Degrees),
|
"deg" => Ok(UnitAngle::Degrees),
|
||||||
"rad" => Ok(UnitAngle::Radians),
|
"rad" => Ok(UnitAngle::Radians),
|
||||||
value => Err(KclError::Semantic(KclErrorDetails {
|
value => Err(KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("Unexpected settings value: `{value}`; expected one of `deg`, `rad`"),
|
message: format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,8 @@ use crate::{
|
|||||||
parsing::ast::types::{
|
parsing::ast::types::{
|
||||||
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||||
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
|
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
|
||||||
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
|
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeNode, NonCodeValue,
|
||||||
PipeExpression, Program, TagDeclarator, UnaryExpression, UnaryOperator,
|
ObjectExpression, PipeExpression, Program, TagDeclarator, UnaryExpression, UnaryOperator,
|
||||||
},
|
},
|
||||||
source_range::SourceRange,
|
source_range::SourceRange,
|
||||||
std::{
|
std::{
|
||||||
@ -110,7 +110,10 @@ impl ExecutorContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let source_range = SourceRange::from(import_stmt);
|
let source_range = SourceRange::from(import_stmt);
|
||||||
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
|
let meta_nodes = program.non_code_meta.get(i);
|
||||||
|
let module_id = self
|
||||||
|
.open_module(&import_stmt.path, meta_nodes, exec_state, source_range)
|
||||||
|
.await?;
|
||||||
|
|
||||||
match &import_stmt.selector {
|
match &import_stmt.selector {
|
||||||
ImportSelector::List { items } => {
|
ImportSelector::List { items } => {
|
||||||
@ -205,13 +208,7 @@ impl ExecutorContext {
|
|||||||
let source_range = SourceRange::from(&variable_declaration.declaration.init);
|
let source_range = SourceRange::from(&variable_declaration.declaration.init);
|
||||||
let metadata = Metadata { source_range };
|
let metadata = Metadata { source_range };
|
||||||
|
|
||||||
let _meta_nodes = if i == 0 {
|
let _meta_nodes = program.non_code_meta.get(i);
|
||||||
&program.non_code_meta.start_nodes
|
|
||||||
} else if let Some(meta) = program.non_code_meta.non_code_nodes.get(&(i - 1)) {
|
|
||||||
meta
|
|
||||||
} else {
|
|
||||||
&Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let memory_item = self
|
let memory_item = self
|
||||||
.execute_expr(
|
.execute_expr(
|
||||||
@ -281,6 +278,7 @@ impl ExecutorContext {
|
|||||||
async fn open_module(
|
async fn open_module(
|
||||||
&self,
|
&self,
|
||||||
path: &ImportPath,
|
path: &ImportPath,
|
||||||
|
non_code_meta: &[Node<NonCodeNode>],
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
) -> Result<ModuleId, KclError> {
|
) -> Result<ModuleId, KclError> {
|
||||||
@ -307,9 +305,9 @@ impl ExecutorContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let id = exec_state.next_module_id();
|
let id = exec_state.next_module_id();
|
||||||
let geom =
|
let path = resolved_path.expect_path();
|
||||||
super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range)
|
let format = super::import::format_from_annotations(non_code_meta, path, source_range)?;
|
||||||
.await?;
|
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
|
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use std::{ffi::OsStr, path::Path, str::FromStr};
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use kcmc::{
|
use kcmc::{
|
||||||
coord::{Axis, AxisDirectionPair, Direction, System},
|
coord::{System, KITTYCAD},
|
||||||
each_cmd as mcmd,
|
each_cmd as mcmd,
|
||||||
format::InputFormat,
|
format::InputFormat,
|
||||||
ok_response::OkModelingCmdResponse,
|
ok_response::OkModelingCmdResponse,
|
||||||
@ -15,11 +15,11 @@ use kittycad_modeling_cmds as kcmc;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::ExecutorContext;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{ExecState, ImportedGeometry},
|
execution::{annotations, kcl_value::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
|
||||||
fs::FileSystem,
|
fs::FileSystem,
|
||||||
|
parsing::ast::types::{Node, NonCodeNode},
|
||||||
source_range::SourceRange,
|
source_range::SourceRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,16 +28,7 @@ use crate::{
|
|||||||
// * Forward: -Y
|
// * Forward: -Y
|
||||||
// * Up: +Z
|
// * Up: +Z
|
||||||
// * Handedness: Right
|
// * Handedness: Right
|
||||||
pub const ZOO_COORD_SYSTEM: System = System {
|
pub const ZOO_COORD_SYSTEM: System = *KITTYCAD;
|
||||||
forward: AxisDirectionPair {
|
|
||||||
axis: Axis::Y,
|
|
||||||
direction: Direction::Negative,
|
|
||||||
},
|
|
||||||
up: AxisDirectionPair {
|
|
||||||
axis: Axis::Z,
|
|
||||||
direction: Direction::Positive,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn import_foreign(
|
pub async fn import_foreign(
|
||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
@ -54,7 +45,8 @@ pub async fn import_foreign(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
|
let ext_format =
|
||||||
|
get_import_format_from_extension(file_path.extension().and_then(OsStr::to_str).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails {
|
||||||
message: format!("No file extension found for `{}`", file_path.display()),
|
message: format!("No file extension found for `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
source_ranges: vec![source_range],
|
||||||
@ -162,6 +154,131 @@ pub async fn import_foreign(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn format_from_annotations(
|
||||||
|
non_code_meta: &[Node<NonCodeNode>],
|
||||||
|
path: &Path,
|
||||||
|
import_source_range: SourceRange,
|
||||||
|
) -> Result<Option<InputFormat>, KclError> {
|
||||||
|
let Some(props) = annotations::unnamed_properties(non_code_meta.iter().map(|n| &n.value)) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = None;
|
||||||
|
for p in props {
|
||||||
|
if p.key.name == annotations::IMPORT_FORMAT {
|
||||||
|
result = Some(
|
||||||
|
get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| {
|
||||||
|
KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"Unknown format for import, expected one of: {}",
|
||||||
|
annotations::IMPORT_FORMAT_VALUES.join(", ")
|
||||||
|
),
|
||||||
|
source_ranges: vec![p.as_source_range()],
|
||||||
|
})
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = result
|
||||||
|
.or_else(|| {
|
||||||
|
path.extension()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.and_then(|ext| get_import_format_from_extension(ext).ok())
|
||||||
|
})
|
||||||
|
.ok_or(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: "Unknown or missing extension, and no specified format for imported file".to_owned(),
|
||||||
|
source_ranges: vec![import_source_range],
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
for p in props {
|
||||||
|
match p.key.name.as_str() {
|
||||||
|
annotations::IMPORT_COORDS => {
|
||||||
|
set_coords(&mut result, annotations::expect_ident(&p.value)?, p.as_source_range())?;
|
||||||
|
}
|
||||||
|
annotations::IMPORT_LENGTH_UNIT => {
|
||||||
|
set_length_unit(&mut result, annotations::expect_ident(&p.value)?, p.as_source_range())?;
|
||||||
|
}
|
||||||
|
annotations::IMPORT_FORMAT => {}
|
||||||
|
_ => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"Unexpected annotation for import, expected one of: {}, {}, {}",
|
||||||
|
annotations::IMPORT_FORMAT,
|
||||||
|
annotations::IMPORT_COORDS,
|
||||||
|
annotations::IMPORT_LENGTH_UNIT
|
||||||
|
),
|
||||||
|
source_ranges: vec![p.as_source_range()],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_coords(fmt: &mut InputFormat, coords_str: &str, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
|
let mut coords = None;
|
||||||
|
for (name, val) in annotations::IMPORT_COORDS_VALUES {
|
||||||
|
if coords_str == name {
|
||||||
|
coords = Some(*val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(coords) = coords else {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"Unknown coordinate system: {coords_str}, expected one of: {}",
|
||||||
|
annotations::IMPORT_COORDS_VALUES
|
||||||
|
.iter()
|
||||||
|
.map(|(n, _)| *n)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
match fmt {
|
||||||
|
InputFormat::Obj(opts) => opts.coords = coords,
|
||||||
|
InputFormat::Ply(opts) => opts.coords = coords,
|
||||||
|
InputFormat::Stl(opts) => opts.coords = coords,
|
||||||
|
_ => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"`{}` option cannot be applied to the specified format",
|
||||||
|
annotations::IMPORT_COORDS
|
||||||
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_length_unit(fmt: &mut InputFormat, units_str: &str, source_range: SourceRange) -> Result<(), KclError> {
|
||||||
|
let units = UnitLen::from_str(units_str, source_range)?;
|
||||||
|
|
||||||
|
match fmt {
|
||||||
|
InputFormat::Obj(opts) => opts.units = units.into(),
|
||||||
|
InputFormat::Ply(opts) => opts.units = units.into(),
|
||||||
|
InputFormat::Stl(opts) => opts.units = units.into(),
|
||||||
|
_ => {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails {
|
||||||
|
message: format!(
|
||||||
|
"`{}` option cannot be applied to the specified format",
|
||||||
|
annotations::IMPORT_LENGTH_UNIT
|
||||||
|
),
|
||||||
|
source_ranges: vec![source_range],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct PreImportedGeometry {
|
pub struct PreImportedGeometry {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@ -201,10 +318,7 @@ pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the source format from the extension.
|
/// Get the source format from the extension.
|
||||||
fn get_import_format_from_extension(ext: &OsStr) -> Result<InputFormat> {
|
fn get_import_format_from_extension(ext: &str) -> Result<InputFormat> {
|
||||||
let ext = ext
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Invalid file extension: `{ext:?}`"))?;
|
|
||||||
let format = match FileImportFormat::from_str(ext) {
|
let format = match FileImportFormat::from_str(ext) {
|
||||||
Ok(format) => format,
|
Ok(format) => format,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -291,3 +405,130 @@ fn get_name_of_format(type_: InputFormat) -> &'static str {
|
|||||||
InputFormat::Stl(_) => "stl",
|
InputFormat::Stl(_) => "stl",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotations() {
|
||||||
|
// no annotations
|
||||||
|
assert!(
|
||||||
|
format_from_annotations(&[], Path::new("../foo.txt"), SourceRange::default(),)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
// no format, no options
|
||||||
|
let text = "@()\nimport '../foo.gltf' as foo";
|
||||||
|
let parsed = crate::Program::parse_no_errs(text).unwrap().ast;
|
||||||
|
let non_code_meta = parsed.non_code_meta.get(0);
|
||||||
|
let fmt = format_from_annotations(non_code_meta, Path::new("../foo.gltf"), SourceRange::default())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fmt,
|
||||||
|
InputFormat::Gltf(kittycad_modeling_cmds::format::gltf::import::Options {})
|
||||||
|
);
|
||||||
|
|
||||||
|
// format, no options
|
||||||
|
let text = "@(format = gltf)\nimport '../foo.txt' as foo";
|
||||||
|
let parsed = crate::Program::parse_no_errs(text).unwrap().ast;
|
||||||
|
let non_code_meta = parsed.non_code_meta.get(0);
|
||||||
|
let fmt = format_from_annotations(non_code_meta, Path::new("../foo.txt"), SourceRange::default())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fmt,
|
||||||
|
InputFormat::Gltf(kittycad_modeling_cmds::format::gltf::import::Options {})
|
||||||
|
);
|
||||||
|
|
||||||
|
// format, no extension (wouldn't parse but might some day)
|
||||||
|
let fmt = format_from_annotations(non_code_meta, Path::new("../foo"), SourceRange::default())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fmt,
|
||||||
|
InputFormat::Gltf(kittycad_modeling_cmds::format::gltf::import::Options {})
|
||||||
|
);
|
||||||
|
|
||||||
|
// format, options
|
||||||
|
let text = "@(format = obj, coords = vulkan, lengthUnit = ft)\nimport '../foo.txt' as foo";
|
||||||
|
let parsed = crate::Program::parse_no_errs(text).unwrap().ast;
|
||||||
|
let non_code_meta = parsed.non_code_meta.get(0);
|
||||||
|
let fmt = format_from_annotations(non_code_meta, Path::new("../foo.txt"), SourceRange::default())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fmt,
|
||||||
|
InputFormat::Obj(kittycad_modeling_cmds::format::obj::import::Options {
|
||||||
|
coords: *kittycad_modeling_cmds::coord::VULKAN,
|
||||||
|
units: kittycad_modeling_cmds::units::UnitLength::Feet,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// no format, options
|
||||||
|
let text = "@(coords = vulkan, lengthUnit = ft)\nimport '../foo.obj' as foo";
|
||||||
|
let parsed = crate::Program::parse_no_errs(text).unwrap().ast;
|
||||||
|
let non_code_meta = parsed.non_code_meta.get(0);
|
||||||
|
let fmt = format_from_annotations(non_code_meta, Path::new("../foo.obj"), SourceRange::default())
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fmt,
|
||||||
|
InputFormat::Obj(kittycad_modeling_cmds::format::obj::import::Options {
|
||||||
|
coords: *kittycad_modeling_cmds::coord::VULKAN,
|
||||||
|
units: kittycad_modeling_cmds::units::UnitLength::Feet,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// err - format, options, but no options for specified format
|
||||||
|
assert_annotation_error(
|
||||||
|
"@(format = gltf, lengthUnit = ft)\nimport '../foo.txt' as foo",
|
||||||
|
"../foo.txt",
|
||||||
|
"`lengthUnit` option cannot be applied",
|
||||||
|
);
|
||||||
|
// err - no format, options, but no options for specified format
|
||||||
|
assert_annotation_error(
|
||||||
|
"@(lengthUnit = ft)\nimport '../foo.gltf' as foo",
|
||||||
|
"../foo.gltf",
|
||||||
|
"lengthUnit` option cannot be applied",
|
||||||
|
);
|
||||||
|
// err - bad option
|
||||||
|
assert_annotation_error(
|
||||||
|
"@(format = obj, coords = vulkan, lengthUni = ft)\nimport '../foo.txt' as foo",
|
||||||
|
"../foo.txt",
|
||||||
|
"Unexpected annotation",
|
||||||
|
);
|
||||||
|
// err - bad format
|
||||||
|
assert_annotation_error(
|
||||||
|
"@(format = foo)\nimport '../foo.txt' as foo",
|
||||||
|
"../foo.txt",
|
||||||
|
"Unknown format for import",
|
||||||
|
);
|
||||||
|
// err - bad coord value
|
||||||
|
assert_annotation_error(
|
||||||
|
"@(format = gltf, coords = north)\nimport '../foo.txt' as foo",
|
||||||
|
"../foo.txt",
|
||||||
|
"Unknown coordinate system",
|
||||||
|
);
|
||||||
|
// err - bad unit value
|
||||||
|
assert_annotation_error(
|
||||||
|
"@(format = gltf, lengthUnit = gallons)\nimport '../foo.txt' as foo",
|
||||||
|
"../foo.txt",
|
||||||
|
"Unexpected value for length units",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_annotation_error(src: &str, path: &str, expected: &str) {
|
||||||
|
let parsed = crate::Program::parse_no_errs(src).unwrap().ast;
|
||||||
|
let non_code_meta = parsed.non_code_meta.get(0);
|
||||||
|
let err = format_from_annotations(non_code_meta, Path::new(path), SourceRange::default()).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.message().contains(expected),
|
||||||
|
"Expected: `{expected}`, found `{}`",
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -674,6 +674,19 @@ impl From<UnitLen> for crate::UnitLength {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<UnitLen> for kittycad_modeling_cmds::units::UnitLength {
|
||||||
|
fn from(unit: UnitLen) -> Self {
|
||||||
|
match unit {
|
||||||
|
UnitLen::Cm => kittycad_modeling_cmds::units::UnitLength::Centimeters,
|
||||||
|
UnitLen::Feet => kittycad_modeling_cmds::units::UnitLength::Feet,
|
||||||
|
UnitLen::Inches => kittycad_modeling_cmds::units::UnitLength::Inches,
|
||||||
|
UnitLen::M => kittycad_modeling_cmds::units::UnitLength::Meters,
|
||||||
|
UnitLen::Mm => kittycad_modeling_cmds::units::UnitLength::Millimeters,
|
||||||
|
UnitLen::Yards => kittycad_modeling_cmds::units::UnitLength::Yards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
|
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
|
|||||||
@ -1271,6 +1271,19 @@ impl NonCodeMeta {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
|
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the non-code meta immediately before the ith node in the AST that self is attached to.
|
||||||
|
///
|
||||||
|
/// Returns an empty slice if there is no non-code metadata associated with the node.
|
||||||
|
pub fn get(&self, i: usize) -> &[Node<NonCodeNode>] {
|
||||||
|
if i == 0 {
|
||||||
|
&self.start_nodes
|
||||||
|
} else if let Some(meta) = self.non_code_nodes.get(&(i - 1)) {
|
||||||
|
meta
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
|
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
|
||||||
@ -1495,7 +1508,7 @@ impl ImportStatement {
|
|||||||
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => s.split('.'),
|
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => s.split('.'),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let name = parts.next()?;
|
let path = parts.next()?;
|
||||||
let _ext = parts.next()?;
|
let _ext = parts.next()?;
|
||||||
let rest = parts.next();
|
let rest = parts.next();
|
||||||
|
|
||||||
@ -1503,7 +1516,7 @@ impl ImportStatement {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(name.to_owned())
|
path.rsplit('/').next().map(str::to_owned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -475,19 +475,21 @@ async fn kcl_test_patterns_circular_3d_tilted_axis() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_file_doesnt_exist() {
|
async fn kcl_test_import_file_doesnt_exist() {
|
||||||
let code = r#"model = import("thing.obj")"#;
|
let code = r#"import 'thing.obj'
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await;
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
result.err().unwrap().to_string(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([8, 27, 0])], message: "File `thing.obj` does not exist." }"#
|
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([0, 18, 0])], message: "File `thing.obj` does not exist." }"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_obj_with_mtl() {
|
async fn kcl_test_import_obj_with_mtl() {
|
||||||
let code = r#"model = import("tests/executor/inputs/cube.obj")"#;
|
let code = r#"import 'tests/executor/inputs/cube.obj'
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_obj_with_mtl", &result);
|
assert_out("import_obj_with_mtl", &result);
|
||||||
@ -495,7 +497,9 @@ async fn kcl_test_import_obj_with_mtl() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_obj_with_mtl_units() {
|
async fn kcl_test_import_obj_with_mtl_units() {
|
||||||
let code = r#"model = import("tests/executor/inputs/cube.obj", {format: "obj", units: "m"})"#;
|
let code = r#"@(format = obj, lengthUnit = m)
|
||||||
|
import 'tests/executor/inputs/cube.obj'
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_obj_with_mtl_units", &result);
|
assert_out("import_obj_with_mtl_units", &result);
|
||||||
@ -503,7 +507,8 @@ async fn kcl_test_import_obj_with_mtl_units() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_stl() {
|
async fn kcl_test_import_stl() {
|
||||||
let code = r#"model = import("tests/executor/inputs/2-5-long-m8-chc-screw.stl")"#;
|
let code = r#"import 'tests/executor/inputs/2-5-long-m8-chc-screw.stl' as screw
|
||||||
|
model = screw"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_stl", &result);
|
assert_out("import_stl", &result);
|
||||||
@ -511,7 +516,8 @@ async fn kcl_test_import_stl() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_gltf_with_bin() {
|
async fn kcl_test_import_gltf_with_bin() {
|
||||||
let code = r#"model = import("tests/executor/inputs/cube.gltf")"#;
|
let code = r#"import 'tests/executor/inputs/cube.gltf'
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_gltf_with_bin", &result);
|
assert_out("import_gltf_with_bin", &result);
|
||||||
@ -519,7 +525,8 @@ async fn kcl_test_import_gltf_with_bin() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_gltf_embedded() {
|
async fn kcl_test_import_gltf_embedded() {
|
||||||
let code = r#"model = import("tests/executor/inputs/cube-embedded.gltf")"#;
|
let code = r#"import 'tests/executor/inputs/cube-embedded.gltf' as cube
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_gltf_embedded", &result);
|
assert_out("import_gltf_embedded", &result);
|
||||||
@ -527,7 +534,8 @@ async fn kcl_test_import_gltf_embedded() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_glb() {
|
async fn kcl_test_import_glb() {
|
||||||
let code = r#"model = import("tests/executor/inputs/cube.glb")"#;
|
let code = r#"import 'tests/executor/inputs/cube.glb'
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_glb", &result);
|
assert_out("import_glb", &result);
|
||||||
@ -535,7 +543,8 @@ async fn kcl_test_import_glb() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_glb_no_assign() {
|
async fn kcl_test_import_glb_no_assign() {
|
||||||
let code = r#"import("tests/executor/inputs/cube.glb")"#;
|
let code = r#"import 'tests/executor/inputs/cube.glb'
|
||||||
|
cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await.unwrap();
|
||||||
assert_out("import_glb_no_assign", &result);
|
assert_out("import_glb_no_assign", &result);
|
||||||
@ -543,13 +552,15 @@ async fn kcl_test_import_glb_no_assign() {
|
|||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_import_ext_doesnt_match() {
|
async fn kcl_test_import_ext_doesnt_match() {
|
||||||
let code = r#"model = import("tests/executor/inputs/cube.gltf", {format: "obj", units: "m"})"#;
|
let code = r#"@(format = obj, lengthUnit = m)
|
||||||
|
import 'tests/executor/inputs/cube.gltf'
|
||||||
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, UnitLength::Mm, None).await;
|
let result = execute_and_snapshot(code, UnitLength::Mm, None).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
result.err().unwrap().to_string(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([8, 78, 0])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
|
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([32, 72, 0])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user