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"
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
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],
|
||||
})),
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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`" }"#
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user