use std::str::FromStr;
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use crate::{
errors::{KclError, KclErrorDetails},
executor::{ImportedGeometry, MemoryItem},
fs::FileSystem,
std::Args,
};
const ZOO_COORD_SYSTEM: kittycad::types::System = kittycad::types::System {
forward: kittycad::types::AxisDirectionPair {
axis: kittycad::types::Axis::Y,
direction: kittycad::types::Direction::Negative,
},
up: kittycad::types::AxisDirectionPair {
axis: kittycad::types::Axis::Z,
direction: kittycad::types::Direction::Positive,
},
};
#[derive(serde :: Serialize, serde :: Deserialize, PartialEq, Debug, Clone, schemars :: JsonSchema)]
#[cfg_attr(feature = "tabled", derive(tabled::Tabled))]
#[serde(tag = "type")]
pub enum ImportFormat {
#[serde(rename = "fbx")]
Fbx {},
#[serde(rename = "gltf")]
Gltf {},
#[serde(rename = "obj")]
Obj {
coords: Option<kittycad::types::System>,
units: kittycad::types::UnitLength,
},
#[serde(rename = "ply")]
Ply {
coords: Option<kittycad::types::System>,
units: kittycad::types::UnitLength,
},
#[serde(rename = "sldprt")]
Sldprt {},
#[serde(rename = "step")]
Step {},
#[serde(rename = "stl")]
Stl {
coords: Option<kittycad::types::System>,
units: kittycad::types::UnitLength,
},
}
impl From<ImportFormat> for kittycad::types::InputFormat {
fn from(format: ImportFormat) -> Self {
match format {
ImportFormat::Fbx {} => kittycad::types::InputFormat::Fbx {},
ImportFormat::Gltf {} => kittycad::types::InputFormat::Gltf {},
ImportFormat::Obj { coords, units } => kittycad::types::InputFormat::Obj {
coords: coords.unwrap_or(ZOO_COORD_SYSTEM),
units,
},
ImportFormat::Ply { coords, units } => kittycad::types::InputFormat::Ply {
coords: coords.unwrap_or(ZOO_COORD_SYSTEM),
units,
},
ImportFormat::Sldprt {} => kittycad::types::InputFormat::Sldprt {},
ImportFormat::Step {} => kittycad::types::InputFormat::Step {},
ImportFormat::Stl { coords, units } => kittycad::types::InputFormat::Stl {
coords: coords.unwrap_or(ZOO_COORD_SYSTEM),
units,
},
}
}
}
pub async fn import(args: Args) -> Result<MemoryItem, KclError> {
let (file_path, options): (String, Option<ImportFormat>) = args.get_import_data()?;
let imported_geometry = inner_import(file_path, options, args).await?;
Ok(MemoryItem::ImportedGeometry(imported_geometry))
}
#[stdlib {
name = "import",
tags = [],
}]
async fn inner_import(
file_path: String,
options: Option<ImportFormat>,
args: Args,
) -> Result<ImportedGeometry, KclError> {
if file_path.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {
message: "No file path was provided.".to_string(),
source_ranges: vec![args.source_range],
}));
}
if !args.ctx.fs.exists(&file_path, args.source_range).await? {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("File `{}` does not exist.", file_path),
source_ranges: vec![args.source_range],
}));
}
let ext_format = get_import_format_from_extension(file_path.split('.').last().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("No file extension found for `{}`", file_path),
source_ranges: vec![args.source_range],
})
})?)
.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
let format = if let Some(options) = options {
let format: kittycad::types::InputFormat = options.into();
validate_extension_format(ext_format, format.clone()).map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
format
} else {
ext_format
};
let file_contents = args.ctx.fs.read(&file_path, args.source_range).await.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
let file_name = std::path::Path::new(&file_path)
.file_name()
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Could not get the file name from the path `{}`", file_path),
source_ranges: vec![args.source_range],
})
})?;
let mut import_files = vec![kittycad::types::ImportFile {
path: file_name.to_string(),
data: file_contents.clone(),
}];
if let kittycad::types::InputFormat::Gltf {} = format {
if !file_contents.starts_with(b"glTF") {
let json = gltf_json::Root::from_slice(&file_contents).map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
for buffer in json.buffers.iter() {
if let Some(uri) = &buffer.uri {
if !uri.starts_with("data:") {
let bin_path = std::path::Path::new(&file_path)
.parent()
.map(|p| p.join(uri))
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Could not get the parent path of the file `{}`", file_path),
source_ranges: vec![args.source_range],
})
})?;
let bin_contents = args.ctx.fs.read(&bin_path, args.source_range).await.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
import_files.push(kittycad::types::ImportFile {
path: uri.to_string(),
data: bin_contents,
});
}
}
}
}
}
if args.ctx.is_mock {
return Ok(ImportedGeometry {
id: uuid::Uuid::new_v4(),
value: import_files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![args.source_range.into()],
});
}
let id = uuid::Uuid::new_v4();
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::ImportFiles {
files: import_files.clone(),
format,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::ImportFiles { data: imported_files },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("ImportFiles response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
Ok(ImportedGeometry {
id: imported_files.object_id,
value: import_files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![args.source_range.into()],
})
}
fn get_import_format_from_extension(ext: &str) -> Result<kittycad::types::InputFormat> {
let format = match kittycad::types::FileImportFormat::from_str(ext) {
Ok(format) => format,
Err(_) => {
if ext == "stp" {
kittycad::types::FileImportFormat::Step
} else if ext == "glb" {
kittycad::types::FileImportFormat::Gltf
} else {
anyhow::bail!("unknown source format for file extension: {}. Try setting the `--src-format` flag explicitly or use a valid format.", ext)
}
}
};
let ul = kittycad::types::UnitLength::Mm;
match format {
kittycad::types::FileImportFormat::Step => Ok(kittycad::types::InputFormat::Step {}),
kittycad::types::FileImportFormat::Stl => Ok(kittycad::types::InputFormat::Stl {
coords: ZOO_COORD_SYSTEM,
units: ul,
}),
kittycad::types::FileImportFormat::Obj => Ok(kittycad::types::InputFormat::Obj {
coords: ZOO_COORD_SYSTEM,
units: ul,
}),
kittycad::types::FileImportFormat::Gltf => Ok(kittycad::types::InputFormat::Gltf {}),
kittycad::types::FileImportFormat::Ply => Ok(kittycad::types::InputFormat::Ply {
coords: ZOO_COORD_SYSTEM,
units: ul,
}),
kittycad::types::FileImportFormat::Fbx => Ok(kittycad::types::InputFormat::Fbx {}),
kittycad::types::FileImportFormat::Sldprt => Ok(kittycad::types::InputFormat::Sldprt {}),
}
}
fn validate_extension_format(ext: kittycad::types::InputFormat, given: kittycad::types::InputFormat) -> Result<()> {
if let kittycad::types::InputFormat::Stl { coords: _, units: _ } = ext {
if let kittycad::types::InputFormat::Stl { coords: _, units: _ } = given {
return Ok(());
}
}
if let kittycad::types::InputFormat::Obj { coords: _, units: _ } = ext {
if let kittycad::types::InputFormat::Obj { coords: _, units: _ } = given {
return Ok(());
}
}
if let kittycad::types::InputFormat::Ply { coords: _, units: _ } = ext {
if let kittycad::types::InputFormat::Ply { coords: _, units: _ } = given {
return Ok(());
}
}
if ext == given {
return Ok(());
}
anyhow::bail!(
"The given format does not match the file extension. Expected: `{}`, Given: `{}`",
get_name_of_format(ext),
get_name_of_format(given)
)
}
fn get_name_of_format(type_: kittycad::types::InputFormat) -> String {
match type_ {
kittycad::types::InputFormat::Fbx {} => "fbx".to_string(),
kittycad::types::InputFormat::Gltf {} => "gltf".to_string(),
kittycad::types::InputFormat::Obj { coords: _, units: _ } => "obj".to_string(),
kittycad::types::InputFormat::Ply { coords: _, units: _ } => "ply".to_string(),
kittycad::types::InputFormat::Sldprt {} => "sldprt".to_string(),
kittycad::types::InputFormat::Step {} => "step".to_string(),
kittycad::types::InputFormat::Stl { coords: _, units: _ } => "stl".to_string(),
}
}