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:
Nick Cameron
2025-02-13 06:24:27 +13:00
committed by GitHub
parent 4c0ea136e0
commit 950f5cebfd
7 changed files with 406 additions and 58 deletions

View File

@ -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"
```
## 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

View File

@ -1,8 +1,10 @@
//! Data on available annotations.
use super::kcl_value::{UnitAngle, UnitLen};
use kittycad_modeling_cmds::coord::{System, KITTYCAD, OPENGL, VULKAN};
use crate::{
errors::KclErrorDetails,
execution::kcl_value::{UnitAngle, UnitLen},
parsing::ast::types::{Expr, Node, NonCodeValue, ObjectProperty},
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_ANGLE: &str = "defaultAngleUnit";
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)]
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> {
match expr {
Expr::Identifier(id) => Ok(&id.name),
@ -57,7 +77,7 @@ impl UnitLen {
"yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails {
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],
})),
@ -71,7 +91,7 @@ impl UnitAngle {
"deg" => Ok(UnitAngle::Degrees),
"rad" => Ok(UnitAngle::Radians),
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],
})),
}

View File

@ -18,8 +18,8 @@ use crate::{
parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeValue, ObjectExpression,
PipeExpression, Program, TagDeclarator, UnaryExpression, UnaryOperator,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, Node, NodeRef, NonCodeNode, NonCodeValue,
ObjectExpression, PipeExpression, Program, TagDeclarator, UnaryExpression, UnaryOperator,
},
source_range::SourceRange,
std::{
@ -110,7 +110,10 @@ impl ExecutorContext {
}
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 {
ImportSelector::List { items } => {
@ -205,13 +208,7 @@ impl ExecutorContext {
let source_range = SourceRange::from(&variable_declaration.declaration.init);
let metadata = Metadata { source_range };
let _meta_nodes = if i == 0 {
&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 _meta_nodes = program.non_code_meta.get(i);
let memory_item = self
.execute_expr(
@ -281,6 +278,7 @@ impl ExecutorContext {
async fn open_module(
&self,
path: &ImportPath,
non_code_meta: &[Node<NonCodeNode>],
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<ModuleId, KclError> {
@ -307,9 +305,9 @@ impl ExecutorContext {
}
let id = exec_state.next_module_id();
let geom =
super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range)
.await?;
let path = resolved_path.expect_path();
let format = super::import::format_from_annotations(non_code_meta, path, source_range)?;
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
Ok(id)
}

View File

@ -2,7 +2,7 @@ use std::{ffi::OsStr, path::Path, str::FromStr};
use anyhow::Result;
use kcmc::{
coord::{Axis, AxisDirectionPair, Direction, System},
coord::{System, KITTYCAD},
each_cmd as mcmd,
format::InputFormat,
ok_response::OkModelingCmdResponse,
@ -15,11 +15,11 @@ use kittycad_modeling_cmds as kcmc;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::ExecutorContext;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, ImportedGeometry},
execution::{annotations, kcl_value::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
fs::FileSystem,
parsing::ast::types::{Node, NonCodeNode},
source_range::SourceRange,
};
@ -28,16 +28,7 @@ use crate::{
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
pub const ZOO_COORD_SYSTEM: System = System {
forward: AxisDirectionPair {
axis: Axis::Y,
direction: Direction::Negative,
},
up: AxisDirectionPair {
axis: Axis::Z,
direction: Direction::Positive,
},
};
pub const ZOO_COORD_SYSTEM: System = *KITTYCAD;
pub async fn import_foreign(
file_path: &Path,
@ -54,18 +45,19 @@ pub async fn import_foreign(
}));
}
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("No file extension found for `{}`", file_path.display()),
source_ranges: vec![source_range],
})
})?)
.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
let ext_format =
get_import_format_from_extension(file_path.extension().and_then(OsStr::to_str).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("No file extension found for `{}`", file_path.display()),
source_ranges: vec![source_range],
})
})?)
.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
// Get the format type from the extension of the file.
let format = if let Some(format) = format {
@ -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)]
pub struct PreImportedGeometry {
id: Uuid,
@ -201,10 +318,7 @@ pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) ->
}
/// Get the source format from the extension.
fn get_import_format_from_extension(ext: &OsStr) -> Result<InputFormat> {
let ext = ext
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid file extension: `{ext:?}`"))?;
fn get_import_format_from_extension(ext: &str) -> Result<InputFormat> {
let format = match FileImportFormat::from_str(ext) {
Ok(format) => format,
Err(_) => {
@ -291,3 +405,130 @@ fn get_name_of_format(type_: InputFormat) -> &'static str {
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()
);
}
}

View File

@ -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)]
#[ts(export)]
#[serde(tag = "type")]

View File

@ -1271,6 +1271,19 @@ impl NonCodeMeta {
.iter()
.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
@ -1495,7 +1508,7 @@ impl ImportStatement {
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => s.split('.'),
_ => return None,
};
let name = parts.next()?;
let path = parts.next()?;
let _ext = parts.next()?;
let rest = parts.next();
@ -1503,7 +1516,7 @@ impl ImportStatement {
return None;
}
Some(name.to_owned())
path.rsplit('/').next().map(str::to_owned)
}
}

View File

@ -475,19 +475,21 @@ async fn kcl_test_patterns_circular_3d_tilted_axis() {
#[tokio::test(flavor = "multi_thread")]
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;
assert!(result.is_err());
assert_eq!(
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")]
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();
assert_out("import_obj_with_mtl", &result);
@ -495,7 +497,9 @@ async fn kcl_test_import_obj_with_mtl() {
#[tokio::test(flavor = "multi_thread")]
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();
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")]
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();
assert_out("import_stl", &result);
@ -511,7 +516,8 @@ async fn kcl_test_import_stl() {
#[tokio::test(flavor = "multi_thread")]
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();
assert_out("import_gltf_with_bin", &result);
@ -519,7 +525,8 @@ async fn kcl_test_import_gltf_with_bin() {
#[tokio::test(flavor = "multi_thread")]
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();
assert_out("import_gltf_embedded", &result);
@ -527,7 +534,8 @@ async fn kcl_test_import_gltf_embedded() {
#[tokio::test(flavor = "multi_thread")]
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();
assert_out("import_glb", &result);
@ -535,7 +543,8 @@ async fn kcl_test_import_glb() {
#[tokio::test(flavor = "multi_thread")]
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();
assert_out("import_glb_no_assign", &result);
@ -543,13 +552,15 @@ async fn kcl_test_import_glb_no_assign() {
#[tokio::test(flavor = "multi_thread")]
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;
assert!(result.is_err());
assert_eq!(
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`" }"#
);
}