Import files (#1393)

* initial shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make file system work locally or with tauri

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixxes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better errrors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* better docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better errors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make no assign work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* closer

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more additions to passing around fs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Jess Frazelle
2024-02-12 12:18:37 -08:00
committed by GitHub
parent b6d6f0f4c1
commit 25260a88c3
36 changed files with 2339 additions and 360 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout
ignore-words-list: crate,everytime,inout,co-ordinate
skip: **/target,node_modules,build,**/Cargo.lock

View File

@ -15212,6 +15212,656 @@
"unpublished": false,
"deprecated": false
},
{
"name": "import",
"summary": "Import a CAD file.",
"description": "For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters. Otherwise you can specify the unit by passing in the options parameter. If you import a gltf file, we will try to find the bin file and import it as well.",
"tags": [],
"args": [
{
"name": "file_path",
"type": "String",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "options",
"type": "ImportFormat",
"schema": {
"description": "Import format specifier",
"oneOf": [
{
"description": "Autodesk Filmbox (FBX) format",
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"fbx"
]
}
}
},
{
"description": "Binary glTF 2.0. We refer to this as glTF since that is how our customers refer to it, but this can also import binary glTF (glb).",
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"gltf"
]
}
}
},
{
"description": "Wavefront OBJ format.",
"type": "object",
"required": [
"type",
"units"
],
"properties": {
"coords": {
"description": "Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system.",
"type": "object",
"required": [
"forward",
"up"
],
"properties": {
"forward": {
"description": "Axis the front face of a model looks along.",
"type": "object",
"required": [
"axis",
"direction"
],
"properties": {
"axis": {
"description": "Axis specifier.",
"oneOf": [
{
"description": "'Y' axis.",
"type": "string",
"enum": [
"y"
]
},
{
"description": "'Z' axis.",
"type": "string",
"enum": [
"z"
]
}
]
},
"direction": {
"description": "Specifies which direction the axis is pointing.",
"oneOf": [
{
"description": "Increasing numbers.",
"type": "string",
"enum": [
"positive"
]
},
{
"description": "Decreasing numbers.",
"type": "string",
"enum": [
"negative"
]
}
]
}
}
},
"up": {
"description": "Axis pointing up and away from a model.",
"type": "object",
"required": [
"axis",
"direction"
],
"properties": {
"axis": {
"description": "Axis specifier.",
"oneOf": [
{
"description": "'Y' axis.",
"type": "string",
"enum": [
"y"
]
},
{
"description": "'Z' axis.",
"type": "string",
"enum": [
"z"
]
}
]
},
"direction": {
"description": "Specifies which direction the axis is pointing.",
"oneOf": [
{
"description": "Increasing numbers.",
"type": "string",
"enum": [
"positive"
]
},
{
"description": "Decreasing numbers.",
"type": "string",
"enum": [
"negative"
]
}
]
}
}
}
},
"nullable": true
},
"type": {
"type": "string",
"enum": [
"obj"
]
},
"units": {
"description": "The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters.",
"oneOf": [
{
"description": "Centimeters <https://en.wikipedia.org/wiki/Centimeter>",
"type": "string",
"enum": [
"cm"
]
},
{
"description": "Feet <https://en.wikipedia.org/wiki/Foot_(unit)>",
"type": "string",
"enum": [
"ft"
]
},
{
"description": "Inches <https://en.wikipedia.org/wiki/Inch>",
"type": "string",
"enum": [
"in"
]
},
{
"description": "Meters <https://en.wikipedia.org/wiki/Meter>",
"type": "string",
"enum": [
"m"
]
},
{
"description": "Millimeters <https://en.wikipedia.org/wiki/Millimeter>",
"type": "string",
"enum": [
"mm"
]
},
{
"description": "Yards <https://en.wikipedia.org/wiki/Yard>",
"type": "string",
"enum": [
"yd"
]
}
]
}
}
},
{
"description": "The PLY Polygon File Format.",
"type": "object",
"required": [
"type",
"units"
],
"properties": {
"coords": {
"description": "Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system.",
"type": "object",
"required": [
"forward",
"up"
],
"properties": {
"forward": {
"description": "Axis the front face of a model looks along.",
"type": "object",
"required": [
"axis",
"direction"
],
"properties": {
"axis": {
"description": "Axis specifier.",
"oneOf": [
{
"description": "'Y' axis.",
"type": "string",
"enum": [
"y"
]
},
{
"description": "'Z' axis.",
"type": "string",
"enum": [
"z"
]
}
]
},
"direction": {
"description": "Specifies which direction the axis is pointing.",
"oneOf": [
{
"description": "Increasing numbers.",
"type": "string",
"enum": [
"positive"
]
},
{
"description": "Decreasing numbers.",
"type": "string",
"enum": [
"negative"
]
}
]
}
}
},
"up": {
"description": "Axis pointing up and away from a model.",
"type": "object",
"required": [
"axis",
"direction"
],
"properties": {
"axis": {
"description": "Axis specifier.",
"oneOf": [
{
"description": "'Y' axis.",
"type": "string",
"enum": [
"y"
]
},
{
"description": "'Z' axis.",
"type": "string",
"enum": [
"z"
]
}
]
},
"direction": {
"description": "Specifies which direction the axis is pointing.",
"oneOf": [
{
"description": "Increasing numbers.",
"type": "string",
"enum": [
"positive"
]
},
{
"description": "Decreasing numbers.",
"type": "string",
"enum": [
"negative"
]
}
]
}
}
}
},
"nullable": true
},
"type": {
"type": "string",
"enum": [
"ply"
]
},
"units": {
"description": "The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters.",
"oneOf": [
{
"description": "Centimeters <https://en.wikipedia.org/wiki/Centimeter>",
"type": "string",
"enum": [
"cm"
]
},
{
"description": "Feet <https://en.wikipedia.org/wiki/Foot_(unit)>",
"type": "string",
"enum": [
"ft"
]
},
{
"description": "Inches <https://en.wikipedia.org/wiki/Inch>",
"type": "string",
"enum": [
"in"
]
},
{
"description": "Meters <https://en.wikipedia.org/wiki/Meter>",
"type": "string",
"enum": [
"m"
]
},
{
"description": "Millimeters <https://en.wikipedia.org/wiki/Millimeter>",
"type": "string",
"enum": [
"mm"
]
},
{
"description": "Yards <https://en.wikipedia.org/wiki/Yard>",
"type": "string",
"enum": [
"yd"
]
}
]
}
}
},
{
"description": "SolidWorks part (SLDPRT) format.",
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"sldprt"
]
}
}
},
{
"description": "ISO 10303-21 (STEP) format.",
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"step"
]
}
}
},
{
"description": "ST**ereo**L**ithography format.",
"type": "object",
"required": [
"type",
"units"
],
"properties": {
"coords": {
"description": "Co-ordinate system of input data. Defaults to the [KittyCAD co-ordinate system.",
"type": "object",
"required": [
"forward",
"up"
],
"properties": {
"forward": {
"description": "Axis the front face of a model looks along.",
"type": "object",
"required": [
"axis",
"direction"
],
"properties": {
"axis": {
"description": "Axis specifier.",
"oneOf": [
{
"description": "'Y' axis.",
"type": "string",
"enum": [
"y"
]
},
{
"description": "'Z' axis.",
"type": "string",
"enum": [
"z"
]
}
]
},
"direction": {
"description": "Specifies which direction the axis is pointing.",
"oneOf": [
{
"description": "Increasing numbers.",
"type": "string",
"enum": [
"positive"
]
},
{
"description": "Decreasing numbers.",
"type": "string",
"enum": [
"negative"
]
}
]
}
}
},
"up": {
"description": "Axis pointing up and away from a model.",
"type": "object",
"required": [
"axis",
"direction"
],
"properties": {
"axis": {
"description": "Axis specifier.",
"oneOf": [
{
"description": "'Y' axis.",
"type": "string",
"enum": [
"y"
]
},
{
"description": "'Z' axis.",
"type": "string",
"enum": [
"z"
]
}
]
},
"direction": {
"description": "Specifies which direction the axis is pointing.",
"oneOf": [
{
"description": "Increasing numbers.",
"type": "string",
"enum": [
"positive"
]
},
{
"description": "Decreasing numbers.",
"type": "string",
"enum": [
"negative"
]
}
]
}
}
}
},
"nullable": true
},
"type": {
"type": "string",
"enum": [
"stl"
]
},
"units": {
"description": "The units of the input data. This is very important for correct scaling and when calculating physics properties like mass, etc. Defaults to millimeters.",
"oneOf": [
{
"description": "Centimeters <https://en.wikipedia.org/wiki/Centimeter>",
"type": "string",
"enum": [
"cm"
]
},
{
"description": "Feet <https://en.wikipedia.org/wiki/Foot_(unit)>",
"type": "string",
"enum": [
"ft"
]
},
{
"description": "Inches <https://en.wikipedia.org/wiki/Inch>",
"type": "string",
"enum": [
"in"
]
},
{
"description": "Meters <https://en.wikipedia.org/wiki/Meter>",
"type": "string",
"enum": [
"m"
]
},
{
"description": "Millimeters <https://en.wikipedia.org/wiki/Millimeter>",
"type": "string",
"enum": [
"mm"
]
},
{
"description": "Yards <https://en.wikipedia.org/wiki/Yard>",
"type": "string",
"enum": [
"yd"
]
}
]
}
}
}
],
"nullable": true
},
"required": true
}
],
"returnValue": {
"name": "",
"type": "ImportedGeometry",
"schema": {
"description": "Data for an imported geometry.",
"type": "object",
"required": [
"__meta",
"id",
"value"
],
"properties": {
"__meta": {
"type": "array",
"items": {
"description": "Metadata.",
"type": "object",
"required": [
"sourceRange"
],
"properties": {
"sourceRange": {
"description": "The source range.",
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 2,
"minItems": 2
}
}
}
},
"id": {
"description": "The ID of the imported geometry.",
"type": "string",
"format": "uuid"
},
"value": {
"description": "The original file paths.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"required": true
},
"unpublished": false,
"deprecated": false
},
{
"name": "lastSegX",
"summary": "Returns the last segment of x.",

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@ import FileMachineProvider from 'components/FileMachineProvider'
import { sep } from '@tauri-apps/api/path'
import { paths } from 'lib/paths'
import { IndexLoaderData, HomeLoaderData } from 'lib/types'
import { fileSystemManager } from 'lang/std/fileSystemManager'
if (VITE_KC_SENTRY_DSN && !TEST) {
Sentry.init({
@ -174,6 +175,10 @@ const router = createBrowserRouter(
const children = await readDir(projectPath, { recursive: true })
kclManager.setCodeAndExecute(code, false)
// Set the file system manager to the project path
// So that WASM gets an updated path for operations
fileSystemManager.dir = projectPath
return {
code,
project: {

View File

@ -0,0 +1,62 @@
import {
readBinaryFile,
exists as tauriExists,
BaseDirectory,
} from '@tauri-apps/api/fs'
import { isTauri } from 'lib/isTauri'
import { join } from '@tauri-apps/api/path'
/// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib
/// when executing code.
class FileSystemManager {
private _dir: string | null = null
get dir() {
if (this._dir === null) {
throw new Error('current project dir is not set')
}
return this._dir
}
set dir(dir: string) {
this._dir = dir
}
readFile(path: string): Promise<Uint8Array | void> {
// Using local file system only works from Tauri.
if (!isTauri()) {
throw new Error(
'This function can only be called from a Tauri application'
)
}
return join(this.dir, path)
.catch((error) => {
throw new Error(`Error reading file: ${error}`)
})
.then((file) => {
return readBinaryFile(file)
})
}
exists(path: string): Promise<boolean | void> {
// Using local file system only works from Tauri.
if (!isTauri()) {
throw new Error(
'This function can only be called from a Tauri application'
)
}
return join(this.dir, path)
.catch((error) => {
throw new Error(`Error checking file exists: ${error}`)
})
.then((file) => {
return tauriExists(file)
})
}
}
export const fileSystemManager = new FileSystemManager()

View File

@ -16,6 +16,7 @@ import { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
import type { Program } from '../wasm-lib/kcl/bindings/Program'
import type { Token } from '../wasm-lib/kcl/bindings/Token'
import { Coords2d } from './std/sketch'
import { fileSystemManager } from 'lang/std/fileSystemManager'
export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -144,7 +145,8 @@ export const _executor = async (
const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node),
JSON.stringify(programMemory),
engineCommandManager
engineCommandManager,
fileSystemManager
)
return memory
} catch (e: any) {

View File

@ -937,7 +937,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"convert_case",
"expectorate",
@ -952,22 +952,6 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "derive-docs"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8511b1486a7ec6820e889c6a2b57b8b699ab5e5eab507ffd1daa0e07871d2305"
dependencies = [
"convert_case",
"once_cell",
"proc-macro2",
"quote",
"regex",
"serde",
"serde_tokenstream",
"syn 2.0.48",
]
[[package]]
name = "diesel_derives"
version = "2.1.2"
@ -1431,6 +1415,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gltf-derive"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "438ffe1a5540d75403feaf23636b164e816e93f6f03131674722b3886ce32a57"
dependencies = [
"inflections",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "gltf-json"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655951ba557f2bc69ea4b0799446bae281fa78efae6319968bdd2c3e9a06d8e1"
dependencies = [
"gltf-derive",
"serde",
"serde_derive",
"serde_json",
]
[[package]]
name = "grackle"
version = "0.1.0"
@ -1880,9 +1888,10 @@ dependencies = [
"criterion",
"dashmap",
"databake",
"derive-docs 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"derive-docs",
"expectorate",
"futures",
"gltf-json",
"insta",
"itertools 0.12.1",
"js-sys",

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.5"
version = "0.1.6"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -194,41 +194,7 @@ fn do_stdlib_inner(
syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
};
let mut ty_string = ty.to_string().replace('&', "").replace("mut", "").replace(' ', "");
if ty_string.starts_with("Args") {
ty_string = "Args".to_string();
}
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
Vec<#ty_ident>
}
} else if ty_string.starts_with("Option<") {
let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
Option<#ty_ident>
}
} else if let Some((inner_array_type, num)) = parse_array_type(&ty_string) {
let ty_string = inner_array_type.to_owned();
let ty_ident = format_ident!("{}", ty_string);
quote! {
[#ty_ident; #num]
}
} else if ty_string.starts_with("Box<") {
let ty_string = ty_string.trim_start_matches("Box<").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
} else {
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
};
let (ty_string, ty_ident) = clean_ty_string(ty.to_string().as_str());
let ty_string = rust_type_to_openapi_type(&ty_string);
@ -499,6 +465,52 @@ impl Parse for ItemFnForSignature {
}
}
fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
let mut ty_string = t.replace('&', "").replace("mut", "").replace(' ', "");
if ty_string.starts_with("Args") {
ty_string = "Args".to_string();
}
let ty_string = ty_string.trim().to_string();
let ty_ident = if ty_string.starts_with("Vec<") {
let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
Vec<#ty_ident>
}
} else if ty_string.starts_with("kittycad::types::") {
let ty_string = ty_string.trim_start_matches("kittycad::types::").trim_end_matches('>');
let ty_ident = format_ident!("{}", ty_string);
quote! {
kittycad::types::#ty_ident
}
} else if ty_string.starts_with("Option<") {
let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
Option<#ty_ident>
}
} else if let Some((inner_array_type, num)) = parse_array_type(&ty_string) {
let ty_string = inner_array_type.to_owned();
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
[#ty_ident; #num]
}
} else if ty_string.starts_with("Box<") {
let ty_string = ty_string.trim_start_matches("Box<").trim_end_matches('>');
let (_, ty_ident) = clean_ty_string(&ty_string);
quote! {
#ty_ident
}
} else {
let ty_ident = format_ident!("{}", ty_string);
quote! {
#ty_ident
}
};
(ty_string, ty_ident)
}
fn rust_type_to_openapi_type(t: &str) -> String {
let mut t = t.to_string();
// Turn vecs into arrays.
@ -689,4 +701,28 @@ mod tests {
assert!(errors.is_empty());
expectorate::assert_contents("tests/array.gen", &openapitor::types::get_text_fmt(&item).unwrap());
}
#[test]
fn test_stdlib_option_input_format() {
let (item, errors) = do_stdlib(
quote! {
name = "import",
},
quote! {
fn inner_import(
/// The args to do shit to.
args: Option<kittycad::types::InputFormat>
) -> Box<f64> {
args
}
},
)
.unwrap();
assert!(errors.is_empty());
expectorate::assert_contents(
"tests/option_input_format.gen",
&openapitor::types::get_text_fmt(&item).unwrap(),
);
}
}

View File

@ -0,0 +1,84 @@
#[allow(non_camel_case_types, missing_docs)]
#[doc = "Std lib function: import"]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)]
#[ts(export)]
pub(crate) struct Import {}
#[allow(non_upper_case_globals, missing_docs)]
#[doc = "Std lib function: import"]
pub(crate) const Import: Import = Import {};
fn boxed_import(
args: crate::std::Args,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>,
>,
>,
> {
Box::pin(import(args))
}
impl crate::docs::StdLibFn for Import {
fn name(&self) -> String {
"import".to_string()
}
fn summary(&self) -> String {
"".to_string()
}
fn description(&self) -> String {
"".to_string()
}
fn tags(&self) -> Vec<String> {
vec![]
}
fn args(&self) -> Vec<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
vec![crate::docs::StdLibFnArg {
name: "args".to_string(),
type_: "kittycad::types::InputFormat".to_string(),
schema: <Option<kittycad::types::InputFormat>>::json_schema(&mut generator),
required: true,
}]
}
fn return_value(&self) -> Option<crate::docs::StdLibFnArg> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
Some(crate::docs::StdLibFnArg {
name: "".to_string(),
type_: "number".to_string(),
schema: f64::json_schema(&mut generator),
required: true,
})
}
fn unpublished(&self) -> bool {
false
}
fn deprecated(&self) -> bool {
false
}
fn std_lib_fn(&self) -> crate::std::StdFn {
boxed_import
}
fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> {
Box::new(self.clone())
}
}
fn inner_import(
#[doc = r" The args to do shit to."] args: Option<kittycad::types::InputFormat>,
) -> Box<f64> {
args
}

View File

@ -17,8 +17,9 @@ async-trait = "0.1.77"
clap = { version = "4.5.0", features = ["cargo", "derive", "env", "unicode"], optional = true }
dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.5" }
# derive-docs = { path = "../derive-docs" }
#derive-docs = { version = "0.1.6" }
derive-docs = { path = "../derive-docs" }
gltf-json = "1.4.0"
kittycad = { workspace = true }
kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }

View File

@ -284,6 +284,25 @@ pub fn get_description_string_from_schema(schema: &schemars::schema::Schema) ->
pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> {
match schema {
schemars::schema::Schema::Object(o) => {
if let Some(enum_values) = &o.enum_values {
let mut parsed_enum_values: Vec<String> = Default::default();
let mut had_enum_string = false;
for enum_value in enum_values {
if let serde_json::value::Value::String(enum_value) = enum_value {
had_enum_string = true;
parsed_enum_values.push(format!("\"{}\"", enum_value));
} else {
had_enum_string = false;
break;
}
}
if had_enum_string {
return Ok((parsed_enum_values.join(" | "), false));
}
}
// Check if there
if let Some(format) = &o.format {
if format == "uuid" {
return Ok((Primitive::Uuid.to_string(), false));
@ -336,6 +355,34 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
if let Some(subschemas) = &o.subschemas {
let mut fn_docs = String::new();
if let Some(items) = &subschemas.one_of {
let mut had_enum_string = false;
let mut parsed_enum_values: Vec<String> = Vec::new();
for item in items {
if let schemars::schema::Schema::Object(o) = item {
if let Some(enum_values) = &o.enum_values {
for enum_value in enum_values {
if let serde_json::value::Value::String(enum_value) = enum_value {
had_enum_string = true;
parsed_enum_values.push(format!("\"{}\"", enum_value));
} else {
had_enum_string = false;
break;
}
}
if !had_enum_string {
break;
}
} else {
had_enum_string = false;
break;
}
} else {
had_enum_string = false;
break;
}
}
if !had_enum_string {
for (i, item) in items.iter().enumerate() {
// Let's print out the object's properties.
fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string());
@ -343,6 +390,9 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
fn_docs.push_str(" |\n");
}
}
} else {
fn_docs.push_str(&parsed_enum_values.join(" | "));
}
} else if let Some(items) = &subschemas.any_of {
for (i, item) in items.iter().enumerate() {
// Let's print out the object's properties.

View File

@ -13,13 +13,13 @@ extern "C" {
#[derive(Debug, Clone)]
pub type EngineCommandManager;
#[wasm_bindgen(method)]
fn sendModelingCommandFromWasm(
#[wasm_bindgen(method, js_name = sendModelingCommandFromWasm, catch)]
fn send_modeling_cmd_from_wasm(
this: &EngineCommandManager,
id: String,
rangeStr: String,
cmdStr: String,
) -> js_sys::Promise;
) -> Result<js_sys::Promise, js_sys::Error>;
}
#[derive(Debug, Clone)]
@ -59,7 +59,13 @@ impl crate::engine::EngineManager for EngineConnection {
let promise = self
.manager
.sendModelingCommandFromWasm(id.to_string(), source_range_str, cmd_str);
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {

View File

@ -15,6 +15,7 @@ use crate::{
ast::types::{BodyItem, FunctionExpression, KclNone, Value},
engine::EngineConnection,
errors::{KclError, KclErrorDetails},
fs::FileManager,
std::{FunctionKind, StdLib},
};
@ -146,6 +147,7 @@ pub enum MemoryItem {
ExtrudeGroups {
value: Vec<Box<ExtrudeGroup>>,
},
ImportedGeometry(ImportedGeometry),
#[ts(skip)]
ExtrudeTransform(Box<ExtrudeTransform>),
#[ts(skip)]
@ -194,6 +196,19 @@ pub enum SketchGroupSet {
SketchGroups(Vec<Box<SketchGroup>>),
}
/// Data for an imported geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ImportedGeometry {
/// The ID of the imported geometry.
pub id: uuid::Uuid,
/// The original file paths.
pub value: Vec<String>,
#[serde(rename = "__meta")]
pub meta: Vec<Metadata>,
}
/// A plane.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -293,6 +308,7 @@ impl From<MemoryItem> for Vec<SourceRange> {
.iter()
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(),
MemoryItem::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
@ -835,9 +851,33 @@ impl Default for PipeInfo {
#[derive(Debug, Clone)]
pub struct ExecutorContext {
pub engine: EngineConnection,
pub fs: FileManager,
pub stdlib: Arc<StdLib>,
}
impl ExecutorContext {
/// Create a new default executor context.
#[cfg(test)]
pub async fn new() -> Result<Self> {
Ok(Self {
engine: EngineConnection::new().await?,
fs: FileManager::new(),
stdlib: Arc::new(StdLib::new()),
})
}
/// Create a new default executor context.
#[cfg(not(test))]
#[cfg(not(target_arch = "wasm32"))]
pub async fn new(ws: reqwest::Upgraded) -> Result<Self> {
Ok(Self {
engine: EngineConnection::new(ws).await?,
fs: FileManager::new(),
stdlib: Arc::new(StdLib::new()),
})
}
}
/// Execute a AST's program.
#[async_recursion(?Send)]
pub async fn execute(
@ -901,6 +941,10 @@ pub async fn execute(
}
memory.return_ = Some(ProgramReturn::Arguments(call_expr.arguments.clone()));
} else {
let args = crate::std::Args::new(args, call_expr.into(), ctx.clone());
let result = func.std_lib_fn()(args).await?;
memory.return_ = Some(ProgramReturn::Value(result));
}
}
FunctionKind::Std(func) => {
@ -1128,11 +1172,7 @@ mod tests {
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast()?;
let mut mem: ProgramMemory = Default::default();
let engine = EngineConnection::new().await?;
let ctx = ExecutorContext {
engine,
stdlib: Arc::new(StdLib::default()),
};
let ctx = ExecutorContext::new().await?;
let memory = execute(program, &mut mem, BodyType::Root, &ctx).await?;
Ok(memory)

View File

@ -0,0 +1,56 @@
//! Functions for interacting with a file system locally.
use anyhow::Result;
use crate::{
errors::{KclError, KclErrorDetails},
fs::FileSystem,
};
#[derive(Debug, Clone)]
pub struct FileManager {}
impl FileManager {
pub fn new() -> FileManager {
FileManager {}
}
}
impl Default for FileManager {
fn default() -> Self {
FileManager::new()
}
}
#[async_trait::async_trait(?Send)]
impl FileSystem for FileManager {
async fn read<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<Vec<u8>, KclError> {
tokio::fs::read(&path).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to read file `{}`: {}", path.as_ref().display(), e),
source_ranges: vec![source_range],
})
})
}
async fn exists<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<bool, crate::errors::KclError> {
tokio::fs::metadata(&path).await.map(|_| true).or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(false)
} else {
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to check if file `{}` exists: {}", path.as_ref().display(), e),
source_ranges: vec![source_range],
}))
}
})
}
}

View File

@ -0,0 +1,32 @@
//! Functions for interacting with files on a machine.
#[cfg(not(target_arch = "wasm32"))]
pub mod local;
#[cfg(not(target_arch = "wasm32"))]
pub use local::FileManager;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub mod wasm;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub use wasm::FileManager;
use anyhow::Result;
#[async_trait::async_trait(?Send)]
pub trait FileSystem: Clone {
/// Read a file from the local file system.
async fn read<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<Vec<u8>, crate::errors::KclError>;
/// Check if a file exists on the local file system.
async fn exists<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<bool, crate::errors::KclError>;
}

View File

@ -0,0 +1,115 @@
//! Functions for interacting with a file system via wasm.
use anyhow::Result;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{
errors::{KclError, KclErrorDetails},
fs::FileSystem,
};
#[wasm_bindgen(module = "/../../lang/std/fileSystemManager.ts")]
extern "C" {
#[derive(Debug, Clone)]
pub type FileSystemManager;
#[wasm_bindgen(method, js_name = readFile, catch)]
fn read_file(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = exists, catch)]
fn exists(this: &FileSystemManager, path: String) -> Result<js_sys::Promise, js_sys::Error>;
}
#[derive(Debug, Clone)]
pub struct FileManager {
manager: FileSystemManager,
}
impl FileManager {
pub fn new(manager: FileSystemManager) -> FileManager {
FileManager { manager }
}
}
#[async_trait::async_trait(?Send)]
impl FileSystem for FileManager {
async fn read<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<Vec<u8>, KclError> {
let promise = self
.manager
.read_file(
path.as_ref()
.to_str()
.ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: "Failed to convert path to string".to_string(),
source_ranges: vec![source_range],
})
})?
.to_string(),
)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let array = js_sys::Uint8Array::new(&value);
let bytes: Vec<u8> = array.to_vec();
Ok(bytes)
}
async fn exists<P: AsRef<std::path::Path>>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<bool, crate::errors::KclError> {
let promise = self
.manager
.exists(
path.as_ref()
.to_str()
.ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: "Failed to convert path to string".to_string(),
source_ranges: vec![source_range],
})
})?
.to_string(),
)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = wasm_bindgen_futures::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let it_exists = value.as_bool().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: "Failed to convert value to bool".to_string(),
source_ranges: vec![source_range],
})
})?;
Ok(it_exists)
}
}

View File

@ -9,6 +9,7 @@ pub mod docs;
pub mod engine;
pub mod errors;
pub mod executor;
pub mod fs;
pub mod parser;
pub mod server;
pub mod std;

View File

@ -0,0 +1,358 @@
//! Standard library functions involved in importing files.
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,
};
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
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,
},
};
/// Import format specifier
#[derive(serde :: Serialize, serde :: Deserialize, PartialEq, Debug, Clone, schemars :: JsonSchema)]
#[cfg_attr(feature = "tabled", derive(tabled::Tabled))]
#[serde(tag = "type")]
pub enum ImportFormat {
/// Autodesk Filmbox (FBX) format
#[serde(rename = "fbx")]
Fbx {},
/// Binary glTF 2.0. We refer to this as glTF since that is how our customers refer to
/// it, but this can also import binary glTF (glb).
#[serde(rename = "gltf")]
Gltf {},
/// Wavefront OBJ format.
#[serde(rename = "obj")]
Obj {
/// Co-ordinate system of input data.
/// Defaults to the [KittyCAD co-ordinate system.
coords: Option<kittycad::types::System>,
/// The units of the input data. This is very important for correct scaling and when
/// calculating physics properties like mass, etc.
/// Defaults to millimeters.
units: kittycad::types::UnitLength,
},
/// The PLY Polygon File Format.
#[serde(rename = "ply")]
Ply {
/// Co-ordinate system of input data.
/// Defaults to the [KittyCAD co-ordinate system.
coords: Option<kittycad::types::System>,
/// The units of the input data. This is very important for correct scaling and when
/// calculating physics properties like mass, etc.
/// Defaults to millimeters.
units: kittycad::types::UnitLength,
},
/// SolidWorks part (SLDPRT) format.
#[serde(rename = "sldprt")]
Sldprt {},
/// ISO 10303-21 (STEP) format.
#[serde(rename = "step")]
Step {},
/// *ST**ereo**L**ithography format.
#[serde(rename = "stl")]
Stl {
/// Co-ordinate system of input data.
/// Defaults to the [KittyCAD co-ordinate system.
coords: Option<kittycad::types::System>,
/// The units of the input data. This is very important for correct scaling and when
/// calculating physics properties like mass, etc.
/// Defaults to millimeters.
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,
},
}
}
}
/// Import a CAD file.
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
/// Otherwise you can specify the unit by passing in the options parameter.
/// If you import a gltf file, we will try to find the bin file and import it as well.
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))
}
/// Import a CAD file.
/// For formats lacking unit data (STL, OBJ, PLY), the default import unit is millimeters.
/// Otherwise you can specify the unit by passing in the options parameter.
/// If you import a gltf file, we will try to find the bin file and import it as well.
#[stdlib {
name = "import",
}]
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],
}));
}
// Make sure the file exists.
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],
})
})?;
// Get the format type from the extension of the file.
let format = if let Some(options) = options {
// Validate the given format with the extension format.
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
};
// Get the file contents for each file path.
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],
})
})?;
// We want the file_path to be without the parent.
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(),
}];
// In the case of a gltf importing a bin file we need to handle that! and figure out where the
// file is relative to our current file.
if let kittycad::types::InputFormat::Gltf {} = format {
// Check if the file is a binary gltf file, in that case we don't need to import the bin
// file.
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],
})
})?;
// Read the gltf file and check if there is a bin file.
for buffer in json.buffers.iter() {
if let Some(uri) = &buffer.uri {
if !uri.starts_with("data:") {
// We want this path relative to the file_path given.
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,
});
}
}
}
}
}
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()],
})
}
/// Get the source format from the extension.
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
)
}
}
};
// Make the default units millimeters.
let ul = kittycad::types::UnitLength::Mm;
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
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(),
}
}

View File

@ -1,6 +1,7 @@
//! Functions implemented for language execution.
pub mod extrude;
pub mod import;
pub mod kcl_stdlib;
pub mod math;
pub mod patterns;
@ -71,6 +72,7 @@ lazy_static! {
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole),
Box::new(crate::std::patterns::PatternLinear),
Box::new(crate::std::import::Import),
Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin),
Box::new(crate::std::math::Tan),
@ -371,6 +373,39 @@ impl Args {
Ok(data)
}
fn get_import_data(&self) -> Result<(String, Option<crate::std::import::ImportFormat>), KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let data: String = serde_json::from_value(first_value).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Expected a file path string: {}", e),
source_ranges: vec![self.source_range],
})
})?;
if let Some(second_value) = self.args.get(1) {
let options: crate::std::import::ImportFormat = serde_json::from_value(second_value.get_json_value()?)
.map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Expected input format data: {}", e),
source_ranges: vec![self.source_range],
})
})?;
Ok((data, Some(options)))
} else {
Ok((data, None))
}
}
fn get_data_and_sketch_group<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<SketchGroup>), KclError> {
let first_value = self
.args

View File

@ -1461,7 +1461,6 @@ async fn inner_hole(
}
SketchGroupSet::SketchGroups(hole_sketch_groups) => {
for hole_sketch_group in hole_sketch_groups {
println!("hole_sketch_group: {:?} {}", hole_sketch_group.id, sketch_group.id);
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid2DAddHole {

View File

@ -17,7 +17,8 @@ use wasm_bindgen::prelude::*;
pub async fn execute_wasm(
program_str: &str,
memory_str: &str,
manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
engine_manager: kcl_lib::engine::conn_wasm::EngineCommandManager,
fs_manager: kcl_lib::fs::wasm::FileSystemManager,
) -> Result<JsValue, String> {
// deserialize the ast from a stringified json
@ -25,11 +26,13 @@ pub async fn execute_wasm(
let program: kcl_lib::ast::types::Program = serde_json::from_str(program_str).map_err(|e| e.to_string())?;
let mut mem: kcl_lib::executor::ProgramMemory = serde_json::from_str(memory_str).map_err(|e| e.to_string())?;
let engine = kcl_lib::engine::EngineConnection::new(manager)
let engine = kcl_lib::engine::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?;
let fs = kcl_lib::fs::FileManager::new(fs_manager);
let ctx = ExecutorContext {
engine,
fs,
stdlib: std::sync::Arc::new(kcl_lib::std::StdLib::new()),
};

View File

@ -0,0 +1,108 @@
{
"asset": {
"generator": "Khronos glTF Blender I/O v3.4.50",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"name": "Scene",
"nodes": [0]
}
],
"nodes": [
{
"mesh": 0,
"name": "Cube"
}
],
"materials": [
{
"doubleSided": true,
"name": "Material",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.800000011920929, 0.800000011920929, 0.800000011920929, 1
],
"metallicFactor": 0,
"roughnessFactor": 0.5
}
}
],
"meshes": [
{
"name": "Cube",
"primitives": [
{
"attributes": {
"POSITION": 0,
"TEXCOORD_0": 1,
"NORMAL": 2
},
"indices": 3,
"material": 0
}
]
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 24,
"type": "VEC2"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 24,
"type": "VEC3"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 36,
"type": "SCALAR"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 288,
"byteOffset": 0,
"target": 34962
},
{
"buffer": 0,
"byteLength": 192,
"byteOffset": 288,
"target": 34962
},
{
"buffer": 0,
"byteLength": 288,
"byteOffset": 480,
"target": 34962
},
{
"buffer": 0,
"byteLength": 72,
"byteOffset": 768,
"target": 34963
}
],
"buffers": [
{
"byteLength": 840,
"uri": "data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AAAAPgAAAD8AAMA+AABAPwAAwD4AAEA/AAAgPwAAAAAAACA/AACAPwAAYD8AAIA+AAAAPgAAgD4AAMA+AAAAAAAAwD4AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAQAOABQAAQAUAAcACgAGABIACgASABYAFwATAAwAFwAMABAADwADAAkADwAJABUABQACAAgABQAIAAsAEQANAAAAEQAAAAQA"
}
]
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,108 @@
{
"asset": {
"generator": "Khronos glTF Blender I/O v3.4.50",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"name": "Scene",
"nodes": [0]
}
],
"nodes": [
{
"mesh": 0,
"name": "Cube"
}
],
"materials": [
{
"doubleSided": true,
"name": "Material",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.800000011920929, 0.800000011920929, 0.800000011920929, 1
],
"metallicFactor": 0,
"roughnessFactor": 0.5
}
}
],
"meshes": [
{
"name": "Cube",
"primitives": [
{
"attributes": {
"POSITION": 0,
"TEXCOORD_0": 1,
"NORMAL": 2
},
"indices": 3,
"material": 0
}
]
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 24,
"type": "VEC2"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 24,
"type": "VEC3"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 36,
"type": "SCALAR"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 288,
"byteOffset": 0,
"target": 34962
},
{
"buffer": 0,
"byteLength": 192,
"byteOffset": 288,
"target": 34962
},
{
"buffer": 0,
"byteLength": 288,
"byteOffset": 480,
"target": 34962
},
{
"buffer": 0,
"byteLength": 72,
"byteOffset": 768,
"target": 34963
}
],
"buffers": [
{
"byteLength": 840,
"uri": "cube.bin"
}
]
}

View File

@ -0,0 +1,12 @@
# Blender MTL File: 'None'
# Material Count: 1
newmtl Material
Ns 359.999993
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

View File

@ -0,0 +1,40 @@
# Blender v3.2.2 OBJ File: ''
# www.blender.org
mtllib cube.mtl
o Cube
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 1.000000 -1.000000
vt 0.625000 0.500000
vt 0.875000 0.500000
vt 0.875000 0.750000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.125000 0.500000
vt 0.375000 0.500000
vt 0.125000 0.750000
vn 0.0000 0.0000 1.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
usemtl Material
s off
f 1/1/1 5/2/1 7/3/1 3/4/1
f 4/5/2 3/4/2 7/6/2 8/7/2
f 8/8/3 7/9/3 5/10/3 6/11/3
f 6/12/4 2/13/4 4/5/4 8/14/4
f 2/13/5 1/1/5 3/4/5 4/5/5
f 6/11/6 5/10/6 1/1/6 2/13/6

View File

@ -1,7 +1,5 @@
use std::sync::Arc;
use anyhow::Result;
use kcl_lib::{engine::EngineManager, std::StdLib};
use kcl_lib::engine::EngineManager;
/// Executes a kcl program and takes a snapshot of the result.
/// This returns the bytes of the snapshot.
@ -38,11 +36,7 @@ async fn execute_and_snapshot(code: &str) -> Result<image::DynamicImage> {
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
let engine = kcl_lib::engine::EngineConnection::new(ws).await?;
let ctx = kcl_lib::executor::ExecutorContext {
engine,
stdlib: Arc::new(StdLib::default()),
};
let ctx = kcl_lib::executor::ExecutorContext::new(ws).await?;
let _ = kcl_lib::executor::execute(program, &mut mem, kcl_lib::executor::BodyType::Root, &ctx).await?;
// Send a snapshot request to the engine.
@ -692,3 +686,75 @@ const rectangle = startSketchOn('XY')
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_file_doesnt_exist() {
let code = r#"const model = import("thing.obj")"#;
let result = execute_and_snapshot(code).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([14, 33])], message: "File `thing.obj` does not exist." }"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_obj_with_mtl() {
let code = r#"const model = import("tests/executor/inputs/cube.obj")"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_obj_with_mtl_units() {
let code = r#"const model = import("tests/executor/inputs/cube.obj", {type: "obj", units: "m"})"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_obj_with_mtl_units.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_gltf_with_bin() {
let code = r#"const model = import("tests/executor/inputs/cube.gltf")"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_with_bin.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_gltf_embedded() {
let code = r#"const model = import("tests/executor/inputs/cube-embedded.gltf")"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_gltf_embedded.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_glb() {
let code = r#"const model = import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_glb_no_assign() {
let code = r#"import("tests/executor/inputs/cube.glb")"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/import_glb_no_assign.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_ext_doesnt_match() {
let code = r#"const model = import("tests/executor/inputs/cube.gltf", {type: "obj", units: "m"})"#;
let result = execute_and_snapshot(code).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([14, 82])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,11 +1,8 @@
use std::sync::Arc;
use anyhow::Result;
use kcl_lib::{
ast::{modify::modify_ast_for_sketch, types::Program},
engine::EngineManager,
executor::{ExecutorContext, MemoryItem, PlaneType, SourceRange},
std::StdLib,
};
use kittycad::types::{ModelingCmd, Point3D};
use pretty_assertions::assert_eq;
@ -40,11 +37,7 @@ async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let mut mem: kcl_lib::executor::ProgramMemory = Default::default();
let engine = kcl_lib::engine::EngineConnection::new(ws).await?;
let ctx = ExecutorContext {
engine,
stdlib: Arc::new(StdLib::default()),
};
let ctx = kcl_lib::executor::ExecutorContext::new(ws).await?;
let memory = kcl_lib::executor::execute(program.clone(), &mut mem, kcl_lib::executor::BodyType::Root, &ctx).await?;
// We need to get the sketch ID.