Compare commits
	
		
			7 Commits
		
	
	
		
			nightly-v2
			...
			achalmers/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eda736a85e | |||
| abbfdae7d2 | |||
| ddbdd9094c | |||
| 7954b6da96 | |||
| bdb84ab3c1 | |||
| 54e160e8d2 | |||
| 2c5a8d439f | 
							
								
								
									
										4
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -2618,9 +2618,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "kittycad" | name = "kittycad" | ||||||
| version = "0.3.5" | version = "0.3.6" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0" | checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "async-trait", |  "async-trait", | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1456,9 +1456,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "kittycad" | name = "kittycad" | ||||||
| version = "0.3.5" | version = "0.3.6" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0" | checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "async-trait", |  "async-trait", | ||||||
|  | |||||||
| @ -69,7 +69,7 @@ members = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
| [workspace.dependencies] | [workspace.dependencies] | ||||||
| kittycad = { version = "0.3.5", default-features = false, features = ["js", "requests"] } | kittycad = { version = "0.3.6", default-features = false, features = ["js", "requests"] } | ||||||
| kittycad-modeling-session = "0.1.4" | kittycad-modeling-session = "0.1.4" | ||||||
|  |  | ||||||
| [[test]] | [[test]] | ||||||
|  | |||||||
| @ -96,11 +96,17 @@ fn do_stdlib_inner( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if !ast.sig.generics.params.is_empty() { |     if !ast.sig.generics.params.is_empty() { | ||||||
|  |         if ast.sig.generics.params.iter().any(|generic_type| match generic_type { | ||||||
|  |             syn::GenericParam::Lifetime(_) => false, | ||||||
|  |             syn::GenericParam::Type(_) => true, | ||||||
|  |             syn::GenericParam::Const(_) => true, | ||||||
|  |         }) { | ||||||
|             errors.push(Error::new_spanned( |             errors.push(Error::new_spanned( | ||||||
|                 &ast.sig.generics, |                 &ast.sig.generics, | ||||||
|             "generics are not permitted for stdlib functions", |                 "Stdlib functions may not be generic over types or constants, only lifetimes.", | ||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if ast.sig.variadic.is_some() { |     if ast.sig.variadic.is_some() { | ||||||
|         errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here")); |         errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here")); | ||||||
| @ -650,7 +656,12 @@ impl Parse for ItemFnForSignature { | |||||||
| } | } | ||||||
|  |  | ||||||
| fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) { | fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) { | ||||||
|     let mut ty_string = t.replace('&', "").replace("mut", "").replace(' ', ""); |     let mut ty_string = t | ||||||
|  |         .replace("& 'a", "") | ||||||
|  |         .replace('&', "") | ||||||
|  |         .replace("mut", "") | ||||||
|  |         .replace("< 'a >", "") | ||||||
|  |         .replace(' ', ""); | ||||||
|     if ty_string.starts_with("Args") { |     if ty_string.starts_with("Args") { | ||||||
|         ty_string = "Args".to_string(); |         ty_string = "Args".to_string(); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -35,6 +35,56 @@ fn test_get_inner_array_type() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[test] | ||||||
|  | fn test_args_with_refs() { | ||||||
|  |     let (item, mut errors) = do_stdlib( | ||||||
|  |         quote! { | ||||||
|  |             name = "someFn", | ||||||
|  |         }, | ||||||
|  |         quote! { | ||||||
|  |             /// Docs | ||||||
|  |             /// ``` | ||||||
|  |             /// someFn() | ||||||
|  |             /// ``` | ||||||
|  |             fn someFn( | ||||||
|  |                 data: &'a str, | ||||||
|  |             ) -> i32 { | ||||||
|  |                 3 | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     .unwrap(); | ||||||
|  |     if let Some(e) = errors.pop() { | ||||||
|  |         panic!("{e}"); | ||||||
|  |     } | ||||||
|  |     expectorate::assert_contents("tests/args_with_refs.gen", &get_text_fmt(&item).unwrap()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[test] | ||||||
|  | fn test_args_with_lifetime() { | ||||||
|  |     let (item, mut errors) = do_stdlib( | ||||||
|  |         quote! { | ||||||
|  |             name = "someFn", | ||||||
|  |         }, | ||||||
|  |         quote! { | ||||||
|  |             /// Docs | ||||||
|  |             /// ``` | ||||||
|  |             /// someFn() | ||||||
|  |             /// ``` | ||||||
|  |             fn someFn<'a>( | ||||||
|  |                 data: Foo<'a>, | ||||||
|  |             ) -> i32 { | ||||||
|  |                 3 | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     .unwrap(); | ||||||
|  |     if let Some(e) = errors.pop() { | ||||||
|  |         panic!("{e}"); | ||||||
|  |     } | ||||||
|  |     expectorate::assert_contents("tests/args_with_lifetime.gen", &get_text_fmt(&item).unwrap()); | ||||||
|  | } | ||||||
|  |  | ||||||
| #[test] | #[test] | ||||||
| fn test_stdlib_line_to() { | fn test_stdlib_line_to() { | ||||||
|     let (item, errors) = do_stdlib( |     let (item, errors) = do_stdlib( | ||||||
| @ -64,7 +114,6 @@ fn test_stdlib_line_to() { | |||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
|     .unwrap(); |     .unwrap(); | ||||||
|     let _expected = quote! {}; |  | ||||||
|  |  | ||||||
|     assert!(errors.is_empty()); |     assert!(errors.is_empty()); | ||||||
|     expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap()); |     expectorate::assert_contents("tests/lineTo.gen", &get_text_fmt(&item).unwrap()); | ||||||
|  | |||||||
							
								
								
									
										194
									
								
								src/wasm-lib/derive-docs/tests/args_with_lifetime.gen
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/wasm-lib/derive-docs/tests/args_with_lifetime.gen
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | |||||||
|  | #[cfg(test)] | ||||||
|  | mod test_examples_someFn { | ||||||
|  |     #[tokio::test(flavor = "multi_thread")] | ||||||
|  |     async fn test_mock_example_someFn0() { | ||||||
|  |         let tokens = crate::token::lexer("someFn()").unwrap(); | ||||||
|  |         let parser = crate::parser::Parser::new(tokens); | ||||||
|  |         let program = parser.ast().unwrap(); | ||||||
|  |         let ctx = crate::executor::ExecutorContext { | ||||||
|  |             engine: std::sync::Arc::new(Box::new( | ||||||
|  |                 crate::engine::conn_mock::EngineConnection::new() | ||||||
|  |                     .await | ||||||
|  |                     .unwrap(), | ||||||
|  |             )), | ||||||
|  |             fs: std::sync::Arc::new(crate::fs::FileManager::new()), | ||||||
|  |             stdlib: std::sync::Arc::new(crate::std::StdLib::new()), | ||||||
|  |             settings: Default::default(), | ||||||
|  |             is_mock: true, | ||||||
|  |         }; | ||||||
|  |         ctx.run(program, None).await.unwrap(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[tokio::test(flavor = "multi_thread", worker_threads = 5)] | ||||||
|  |     async fn serial_test_example_someFn0() { | ||||||
|  |         let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),); | ||||||
|  |         let http_client = reqwest::Client::builder() | ||||||
|  |             .user_agent(user_agent) | ||||||
|  |             .timeout(std::time::Duration::from_secs(600)) | ||||||
|  |             .connect_timeout(std::time::Duration::from_secs(60)); | ||||||
|  |         let ws_client = reqwest::Client::builder() | ||||||
|  |             .user_agent(user_agent) | ||||||
|  |             .timeout(std::time::Duration::from_secs(600)) | ||||||
|  |             .connect_timeout(std::time::Duration::from_secs(60)) | ||||||
|  |             .connection_verbose(true) | ||||||
|  |             .tcp_keepalive(std::time::Duration::from_secs(600)) | ||||||
|  |             .http1_only(); | ||||||
|  |         let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set"); | ||||||
|  |         let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client); | ||||||
|  |         if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") { | ||||||
|  |             client.set_base_url(addr); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let tokens = crate::token::lexer("someFn()").unwrap(); | ||||||
|  |         let parser = crate::parser::Parser::new(tokens); | ||||||
|  |         let program = parser.ast().unwrap(); | ||||||
|  |         let ctx = crate::executor::ExecutorContext::new(&client, Default::default()) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         ctx.run(program, None).await.unwrap(); | ||||||
|  |         ctx.engine | ||||||
|  |             .send_modeling_cmd( | ||||||
|  |                 uuid::Uuid::new_v4(), | ||||||
|  |                 crate::executor::SourceRange::default(), | ||||||
|  |                 kittycad::types::ModelingCmd::ZoomToFit { | ||||||
|  |                     object_ids: Default::default(), | ||||||
|  |                     padding: 0.1, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         let resp = ctx | ||||||
|  |             .engine | ||||||
|  |             .send_modeling_cmd( | ||||||
|  |                 uuid::Uuid::new_v4(), | ||||||
|  |                 crate::executor::SourceRange::default(), | ||||||
|  |                 kittycad::types::ModelingCmd::TakeSnapshot { | ||||||
|  |                     format: kittycad::types::ImageFormat::Png, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         let output_file = | ||||||
|  |             std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4())); | ||||||
|  |         if let kittycad::types::OkWebSocketResponseData::Modeling { | ||||||
|  |             modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data }, | ||||||
|  |         } = &resp | ||||||
|  |         { | ||||||
|  |             std::fs::write(&output_file, &data.contents.0).unwrap(); | ||||||
|  |         } else { | ||||||
|  |             panic!("Unexpected response from engine: {:?}", resp); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let actual = image::io::Reader::open(output_file) | ||||||
|  |             .unwrap() | ||||||
|  |             .decode() | ||||||
|  |             .unwrap(); | ||||||
|  |         twenty_twenty::assert_image( | ||||||
|  |             &format!("tests/outputs/{}.png", "serial_test_example_someFn0"), | ||||||
|  |             &actual, | ||||||
|  |             1.0, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[allow(non_camel_case_types, missing_docs)] | ||||||
|  | #[doc = "Std lib function: someFn\nDocs"] | ||||||
|  | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)] | ||||||
|  | #[ts(export)] | ||||||
|  | pub(crate) struct SomeFn {} | ||||||
|  |  | ||||||
|  | #[allow(non_upper_case_globals, missing_docs)] | ||||||
|  | #[doc = "Std lib function: someFn\nDocs"] | ||||||
|  | pub(crate) const SomeFn: SomeFn = SomeFn {}; | ||||||
|  | fn boxed_someFn( | ||||||
|  |     args: crate::std::Args, | ||||||
|  | ) -> std::pin::Pin< | ||||||
|  |     Box< | ||||||
|  |         dyn std::future::Future< | ||||||
|  |                 Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>, | ||||||
|  |             > + Send, | ||||||
|  |     >, | ||||||
|  | > { | ||||||
|  |     Box::pin(someFn(args)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl crate::docs::StdLibFn for SomeFn { | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         "someFn".to_string() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn summary(&self) -> String { | ||||||
|  |         "Docs".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: "data".to_string(), | ||||||
|  |             type_: "Foo".to_string(), | ||||||
|  |             schema: Foo::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_: "i32".to_string(), | ||||||
|  |             schema: <i32>::json_schema(&mut generator), | ||||||
|  |             required: true, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn unpublished(&self) -> bool { | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn deprecated(&self) -> bool { | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn examples(&self) -> Vec<String> { | ||||||
|  |         let code_blocks = vec!["someFn()"]; | ||||||
|  |         code_blocks | ||||||
|  |             .iter() | ||||||
|  |             .map(|cb| { | ||||||
|  |                 let tokens = crate::token::lexer(cb).unwrap(); | ||||||
|  |                 let parser = crate::parser::Parser::new(tokens); | ||||||
|  |                 let program = parser.ast().unwrap(); | ||||||
|  |                 let mut options: crate::ast::types::FormatOptions = Default::default(); | ||||||
|  |                 options.insert_final_newline = false; | ||||||
|  |                 program.recast(&options, 0) | ||||||
|  |             }) | ||||||
|  |             .collect::<Vec<String>>() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn std_lib_fn(&self) -> crate::std::StdFn { | ||||||
|  |         boxed_someFn | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[doc = r" Docs"] | ||||||
|  | #[doc = r" ```"] | ||||||
|  | #[doc = r" someFn()"] | ||||||
|  | #[doc = r" ```"] | ||||||
|  | fn someFn<'a>(data: Foo<'a>) -> i32 { | ||||||
|  |     3 | ||||||
|  | } | ||||||
							
								
								
									
										194
									
								
								src/wasm-lib/derive-docs/tests/args_with_refs.gen
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/wasm-lib/derive-docs/tests/args_with_refs.gen
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | |||||||
|  | #[cfg(test)] | ||||||
|  | mod test_examples_someFn { | ||||||
|  |     #[tokio::test(flavor = "multi_thread")] | ||||||
|  |     async fn test_mock_example_someFn0() { | ||||||
|  |         let tokens = crate::token::lexer("someFn()").unwrap(); | ||||||
|  |         let parser = crate::parser::Parser::new(tokens); | ||||||
|  |         let program = parser.ast().unwrap(); | ||||||
|  |         let ctx = crate::executor::ExecutorContext { | ||||||
|  |             engine: std::sync::Arc::new(Box::new( | ||||||
|  |                 crate::engine::conn_mock::EngineConnection::new() | ||||||
|  |                     .await | ||||||
|  |                     .unwrap(), | ||||||
|  |             )), | ||||||
|  |             fs: std::sync::Arc::new(crate::fs::FileManager::new()), | ||||||
|  |             stdlib: std::sync::Arc::new(crate::std::StdLib::new()), | ||||||
|  |             settings: Default::default(), | ||||||
|  |             is_mock: true, | ||||||
|  |         }; | ||||||
|  |         ctx.run(program, None).await.unwrap(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #[tokio::test(flavor = "multi_thread", worker_threads = 5)] | ||||||
|  |     async fn serial_test_example_someFn0() { | ||||||
|  |         let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),); | ||||||
|  |         let http_client = reqwest::Client::builder() | ||||||
|  |             .user_agent(user_agent) | ||||||
|  |             .timeout(std::time::Duration::from_secs(600)) | ||||||
|  |             .connect_timeout(std::time::Duration::from_secs(60)); | ||||||
|  |         let ws_client = reqwest::Client::builder() | ||||||
|  |             .user_agent(user_agent) | ||||||
|  |             .timeout(std::time::Duration::from_secs(600)) | ||||||
|  |             .connect_timeout(std::time::Duration::from_secs(60)) | ||||||
|  |             .connection_verbose(true) | ||||||
|  |             .tcp_keepalive(std::time::Duration::from_secs(600)) | ||||||
|  |             .http1_only(); | ||||||
|  |         let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set"); | ||||||
|  |         let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client); | ||||||
|  |         if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") { | ||||||
|  |             client.set_base_url(addr); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let tokens = crate::token::lexer("someFn()").unwrap(); | ||||||
|  |         let parser = crate::parser::Parser::new(tokens); | ||||||
|  |         let program = parser.ast().unwrap(); | ||||||
|  |         let ctx = crate::executor::ExecutorContext::new(&client, Default::default()) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         ctx.run(program, None).await.unwrap(); | ||||||
|  |         ctx.engine | ||||||
|  |             .send_modeling_cmd( | ||||||
|  |                 uuid::Uuid::new_v4(), | ||||||
|  |                 crate::executor::SourceRange::default(), | ||||||
|  |                 kittycad::types::ModelingCmd::ZoomToFit { | ||||||
|  |                     object_ids: Default::default(), | ||||||
|  |                     padding: 0.1, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         let resp = ctx | ||||||
|  |             .engine | ||||||
|  |             .send_modeling_cmd( | ||||||
|  |                 uuid::Uuid::new_v4(), | ||||||
|  |                 crate::executor::SourceRange::default(), | ||||||
|  |                 kittycad::types::ModelingCmd::TakeSnapshot { | ||||||
|  |                     format: kittycad::types::ImageFormat::Png, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         let output_file = | ||||||
|  |             std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4())); | ||||||
|  |         if let kittycad::types::OkWebSocketResponseData::Modeling { | ||||||
|  |             modeling_response: kittycad::types::OkModelingCmdResponse::TakeSnapshot { data }, | ||||||
|  |         } = &resp | ||||||
|  |         { | ||||||
|  |             std::fs::write(&output_file, &data.contents.0).unwrap(); | ||||||
|  |         } else { | ||||||
|  |             panic!("Unexpected response from engine: {:?}", resp); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let actual = image::io::Reader::open(output_file) | ||||||
|  |             .unwrap() | ||||||
|  |             .decode() | ||||||
|  |             .unwrap(); | ||||||
|  |         twenty_twenty::assert_image( | ||||||
|  |             &format!("tests/outputs/{}.png", "serial_test_example_someFn0"), | ||||||
|  |             &actual, | ||||||
|  |             1.0, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[allow(non_camel_case_types, missing_docs)] | ||||||
|  | #[doc = "Std lib function: someFn\nDocs"] | ||||||
|  | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars :: JsonSchema, ts_rs :: TS)] | ||||||
|  | #[ts(export)] | ||||||
|  | pub(crate) struct SomeFn {} | ||||||
|  |  | ||||||
|  | #[allow(non_upper_case_globals, missing_docs)] | ||||||
|  | #[doc = "Std lib function: someFn\nDocs"] | ||||||
|  | pub(crate) const SomeFn: SomeFn = SomeFn {}; | ||||||
|  | fn boxed_someFn( | ||||||
|  |     args: crate::std::Args, | ||||||
|  | ) -> std::pin::Pin< | ||||||
|  |     Box< | ||||||
|  |         dyn std::future::Future< | ||||||
|  |                 Output = anyhow::Result<crate::executor::MemoryItem, crate::errors::KclError>, | ||||||
|  |             > + Send, | ||||||
|  |     >, | ||||||
|  | > { | ||||||
|  |     Box::pin(someFn(args)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl crate::docs::StdLibFn for SomeFn { | ||||||
|  |     fn name(&self) -> String { | ||||||
|  |         "someFn".to_string() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn summary(&self) -> String { | ||||||
|  |         "Docs".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: "data".to_string(), | ||||||
|  |             type_: "string".to_string(), | ||||||
|  |             schema: str::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_: "i32".to_string(), | ||||||
|  |             schema: <i32>::json_schema(&mut generator), | ||||||
|  |             required: true, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn unpublished(&self) -> bool { | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn deprecated(&self) -> bool { | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn examples(&self) -> Vec<String> { | ||||||
|  |         let code_blocks = vec!["someFn()"]; | ||||||
|  |         code_blocks | ||||||
|  |             .iter() | ||||||
|  |             .map(|cb| { | ||||||
|  |                 let tokens = crate::token::lexer(cb).unwrap(); | ||||||
|  |                 let parser = crate::parser::Parser::new(tokens); | ||||||
|  |                 let program = parser.ast().unwrap(); | ||||||
|  |                 let mut options: crate::ast::types::FormatOptions = Default::default(); | ||||||
|  |                 options.insert_final_newline = false; | ||||||
|  |                 program.recast(&options, 0) | ||||||
|  |             }) | ||||||
|  |             .collect::<Vec<String>>() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn std_lib_fn(&self) -> crate::std::StdFn { | ||||||
|  |         boxed_someFn | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn clone_box(&self) -> Box<dyn crate::docs::StdLibFn> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[doc = r" Docs"] | ||||||
|  | #[doc = r" ```"] | ||||||
|  | #[doc = r" someFn()"] | ||||||
|  | #[doc = r" ```"] | ||||||
|  | fn someFn(data: &'a str) -> i32 { | ||||||
|  |     3 | ||||||
|  | } | ||||||
| @ -1135,7 +1135,7 @@ impl CallExpression { | |||||||
|         match ctx.stdlib.get_either(&self.callee.name) { |         match ctx.stdlib.get_either(&self.callee.name) { | ||||||
|             FunctionKind::Core(func) => { |             FunctionKind::Core(func) => { | ||||||
|                 // Attempt to call the function. |                 // Attempt to call the function. | ||||||
|                 let args = crate::std::Args::new(fn_args, self.into(), ctx.clone()); |                 let args = crate::std::Args::new(fn_args, self.into(), ctx.clone(), memory.clone()); | ||||||
|                 let result = func.std_lib_fn()(args).await?; |                 let result = func.std_lib_fn()(args).await?; | ||||||
|                 Ok(result) |                 Ok(result) | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -189,6 +189,15 @@ pub enum SketchGroupSet { | |||||||
|     SketchGroups(Vec<Box<SketchGroup>>), |     SketchGroups(Vec<Box<SketchGroup>>), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl SketchGroupSet { | ||||||
|  |     pub fn ids(&self) -> Vec<uuid::Uuid> { | ||||||
|  |         match self { | ||||||
|  |             SketchGroupSet::SketchGroup(s) => vec![s.id], | ||||||
|  |             SketchGroupSet::SketchGroups(s) => s.iter().map(|s| s.id).collect(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| /// A extrude group or a group of extrude groups. | /// A extrude group or a group of extrude groups. | ||||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||||
| #[ts(export)] | #[ts(export)] | ||||||
| @ -198,6 +207,15 @@ pub enum ExtrudeGroupSet { | |||||||
|     ExtrudeGroups(Vec<Box<ExtrudeGroup>>), |     ExtrudeGroups(Vec<Box<ExtrudeGroup>>), | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl ExtrudeGroupSet { | ||||||
|  |     pub fn ids(&self) -> Vec<uuid::Uuid> { | ||||||
|  |         match self { | ||||||
|  |             ExtrudeGroupSet::ExtrudeGroup(s) => vec![s.id], | ||||||
|  |             ExtrudeGroupSet::ExtrudeGroups(s) => s.iter().map(|s| s.id).collect(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Data for an imported geometry. | /// Data for an imported geometry. | ||||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||||
| #[ts(export)] | #[ts(export)] | ||||||
| @ -298,6 +316,39 @@ pub struct UserVal { | |||||||
|     pub meta: Vec<Metadata>, |     pub meta: Vec<Metadata>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// A function being used as a parameter into a stdlib function. | ||||||
|  | pub struct FunctionParam<'a> { | ||||||
|  |     pub inner: &'a MemoryFunction, | ||||||
|  |     pub memory: ProgramMemory, | ||||||
|  |     pub fn_expr: Box<FunctionExpression>, | ||||||
|  |     pub meta: Vec<Metadata>, | ||||||
|  |     pub ctx: ExecutorContext, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> FunctionParam<'a> { | ||||||
|  |     pub async fn call(&self, args: Vec<MemoryItem>) -> Result<Option<ProgramReturn>, KclError> { | ||||||
|  |         (self.inner)( | ||||||
|  |             args, | ||||||
|  |             self.memory.clone(), | ||||||
|  |             self.fn_expr.clone(), | ||||||
|  |             self.meta.clone(), | ||||||
|  |             self.ctx.clone(), | ||||||
|  |         ) | ||||||
|  |         .await | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> JsonSchema for FunctionParam<'a> { | ||||||
|  |     fn schema_name() -> String { | ||||||
|  |         "FunctionParam".to_owned() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { | ||||||
|  |         // TODO: Actually generate a reasonable schema. | ||||||
|  |         gen.subschema_for::<()>() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| pub type MemoryFunction = | pub type MemoryFunction = | ||||||
|     fn( |     fn( | ||||||
|         s: Vec<MemoryItem>, |         s: Vec<MemoryItem>, | ||||||
| @ -413,6 +464,88 @@ impl MemoryItem { | |||||||
|         }; |         }; | ||||||
|         func(args, memory, expression.clone(), meta.clone(), ctx).await |         func(args, memory, expression.clone(), meta.clone(), ctx).await | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fn as_user_val(&self) -> Option<&UserVal> { | ||||||
|  |         if let MemoryItem::UserVal(x) = self { | ||||||
|  |             Some(x) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// If this value is of type function, return it. | ||||||
|  |     pub fn get_function( | ||||||
|  |         &self, | ||||||
|  |         source_ranges: Vec<SourceRange>, | ||||||
|  |     ) -> Result<(&MemoryFunction, Box<FunctionExpression>), KclError> { | ||||||
|  |         let MemoryItem::Function { | ||||||
|  |             func, | ||||||
|  |             expression, | ||||||
|  |             meta: _, | ||||||
|  |         } = &self | ||||||
|  |         else { | ||||||
|  |             return Err(KclError::Semantic(KclErrorDetails { | ||||||
|  |                 message: "not an in-memory function".to_string(), | ||||||
|  |                 source_ranges, | ||||||
|  |             })); | ||||||
|  |         }; | ||||||
|  |         let func = func.as_ref().ok_or_else(|| { | ||||||
|  |             KclError::Semantic(KclErrorDetails { | ||||||
|  |                 message: format!("Not an in-memory function: {:?}", expression), | ||||||
|  |                 source_ranges, | ||||||
|  |             }) | ||||||
|  |         })?; | ||||||
|  |         Ok((func, expression.to_owned())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// If this value is of type u32, return it. | ||||||
|  |     pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> { | ||||||
|  |         let err = KclError::Semantic(KclErrorDetails { | ||||||
|  |             message: "Expected an integer >= 0".to_owned(), | ||||||
|  |             source_ranges, | ||||||
|  |         }); | ||||||
|  |         self.as_user_val() | ||||||
|  |             .and_then(|uv| uv.value.as_number()) | ||||||
|  |             .and_then(|n| n.as_u64()) | ||||||
|  |             .and_then(|n| u32::try_from(n).ok()) | ||||||
|  |             .ok_or(err) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// If this contains a sketch group set, return it. | ||||||
|  |     pub(crate) fn as_sketch_group_set(&self, sr: SourceRange) -> Result<SketchGroupSet, KclError> { | ||||||
|  |         let sketch_set = if let MemoryItem::SketchGroup(sg) = self { | ||||||
|  |             SketchGroupSet::SketchGroup(sg.clone()) | ||||||
|  |         } else if let MemoryItem::SketchGroups { value } = self { | ||||||
|  |             SketchGroupSet::SketchGroups(value.clone()) | ||||||
|  |         } else { | ||||||
|  |             return Err(KclError::Type(KclErrorDetails { | ||||||
|  |                 message: format!( | ||||||
|  |                     "Expected a SketchGroup or Vector of SketchGroups as this argument, found {:?}", | ||||||
|  |                     self, | ||||||
|  |                 ), | ||||||
|  |                 source_ranges: vec![sr], | ||||||
|  |             })); | ||||||
|  |         }; | ||||||
|  |         Ok(sketch_set) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// If this contains an extrude group set, return it. | ||||||
|  |     pub(crate) fn as_extrude_group_set(&self, sr: SourceRange) -> Result<ExtrudeGroupSet, KclError> { | ||||||
|  |         let sketch_set = if let MemoryItem::ExtrudeGroup(sg) = self { | ||||||
|  |             ExtrudeGroupSet::ExtrudeGroup(sg.clone()) | ||||||
|  |         } else if let MemoryItem::ExtrudeGroups { value } = self { | ||||||
|  |             ExtrudeGroupSet::ExtrudeGroups(value.clone()) | ||||||
|  |         } else { | ||||||
|  |             return Err(KclError::Type(KclErrorDetails { | ||||||
|  |                 message: format!( | ||||||
|  |                     "Expected an ExtrudeGroup or Vector of ExtrudeGroups as this argument, found {:?}", | ||||||
|  |                     self, | ||||||
|  |                 ), | ||||||
|  |                 source_ranges: vec![sr], | ||||||
|  |             })); | ||||||
|  |         }; | ||||||
|  |         Ok(sketch_set) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /// A sketch group is a collection of paths. | /// A sketch group is a collection of paths. | ||||||
| @ -1193,7 +1326,7 @@ impl ExecutorContext { | |||||||
|                         } |                         } | ||||||
|                         match self.stdlib.get_either(&call_expr.callee.name) { |                         match self.stdlib.get_either(&call_expr.callee.name) { | ||||||
|                             FunctionKind::Core(func) => { |                             FunctionKind::Core(func) => { | ||||||
|                                 let args = crate::std::Args::new(args, call_expr.into(), self.clone()); |                                 let args = crate::std::Args::new(args, call_expr.into(), self.clone(), memory.clone()); | ||||||
|                                 let result = func.std_lib_fn()(args).await?; |                                 let result = func.std_lib_fn()(args).await?; | ||||||
|                                 memory.return_ = Some(ProgramReturn::Value(result)); |                                 memory.return_ = Some(ProgramReturn::Value(result)); | ||||||
|                             } |                             } | ||||||
|  | |||||||
| @ -25,14 +25,15 @@ use lazy_static::lazy_static; | |||||||
| use parse_display::{Display, FromStr}; | use parse_display::{Display, FromStr}; | ||||||
| use schemars::JsonSchema; | use schemars::JsonSchema; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  | use uuid::Uuid; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     ast::types::parse_json_number_as_f64, |     ast::types::{parse_json_number_as_f64, FunctionExpression}, | ||||||
|     docs::StdLibFn, |     docs::StdLibFn, | ||||||
|     errors::{KclError, KclErrorDetails}, |     errors::{KclError, KclErrorDetails}, | ||||||
|     executor::{ |     executor::{ | ||||||
|         ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryItem, Metadata, SketchGroup, SketchGroupSet, |         ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryFunction, MemoryItem, Metadata, ProgramMemory, | ||||||
|         SketchSurface, SourceRange, |         SketchGroup, SketchGroupSet, SketchSurface, SourceRange, | ||||||
|     }, |     }, | ||||||
|     std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}, |     std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}, | ||||||
| }; | }; | ||||||
| @ -84,6 +85,7 @@ lazy_static! { | |||||||
|         Box::new(crate::std::patterns::PatternLinear3D), |         Box::new(crate::std::patterns::PatternLinear3D), | ||||||
|         Box::new(crate::std::patterns::PatternCircular2D), |         Box::new(crate::std::patterns::PatternCircular2D), | ||||||
|         Box::new(crate::std::patterns::PatternCircular3D), |         Box::new(crate::std::patterns::PatternCircular3D), | ||||||
|  |         Box::new(crate::std::patterns::Pattern), | ||||||
|         Box::new(crate::std::chamfer::Chamfer), |         Box::new(crate::std::chamfer::Chamfer), | ||||||
|         Box::new(crate::std::fillet::Fillet), |         Box::new(crate::std::fillet::Fillet), | ||||||
|         Box::new(crate::std::fillet::GetOppositeEdge), |         Box::new(crate::std::fillet::GetOppositeEdge), | ||||||
| @ -204,14 +206,17 @@ pub struct Args { | |||||||
|     pub args: Vec<MemoryItem>, |     pub args: Vec<MemoryItem>, | ||||||
|     pub source_range: SourceRange, |     pub source_range: SourceRange, | ||||||
|     pub ctx: ExecutorContext, |     pub ctx: ExecutorContext, | ||||||
|  |     // TODO: This should be reference, not clone. | ||||||
|  |     pub memory: ProgramMemory, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Args { | impl Args { | ||||||
|     pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, ctx: ExecutorContext) -> Self { |     pub fn new(args: Vec<MemoryItem>, source_range: SourceRange, ctx: ExecutorContext, memory: ProgramMemory) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             args, |             args, | ||||||
|             source_range, |             source_range, | ||||||
|             ctx, |             ctx, | ||||||
|  |             memory, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -387,6 +392,41 @@ impl Args { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Works with either 2D or 3D solids. | ||||||
|  |     fn get_pattern_args(&self) -> std::result::Result<(u32, FnAsArg<'_>, Vec<Uuid>), KclError> { | ||||||
|  |         let sr = vec![self.source_range]; | ||||||
|  |         let mut args = self.args.iter(); | ||||||
|  |         let num_repetitions = args.next().ok_or_else(|| { | ||||||
|  |             KclError::Type(KclErrorDetails { | ||||||
|  |                 message: "Missing first argument (should be the number of repetitions)".to_owned(), | ||||||
|  |                 source_ranges: sr.clone(), | ||||||
|  |             }) | ||||||
|  |         })?; | ||||||
|  |         let num_repetitions = num_repetitions.get_u32(sr.clone())?; | ||||||
|  |         let transform = args.next().ok_or_else(|| { | ||||||
|  |             KclError::Type(KclErrorDetails { | ||||||
|  |                 message: "Missing second argument (should be the transform function)".to_owned(), | ||||||
|  |                 source_ranges: sr.clone(), | ||||||
|  |             }) | ||||||
|  |         })?; | ||||||
|  |         let (transform, expr) = transform.get_function(sr.clone())?; | ||||||
|  |         let sg = args.next().ok_or_else(|| { | ||||||
|  |             KclError::Type(KclErrorDetails { | ||||||
|  |                 message: "Missing third argument (should be a Sketch/ExtrudeGroup or an array of Sketch/ExtrudeGroups)" | ||||||
|  |                     .to_owned(), | ||||||
|  |                 source_ranges: sr.clone(), | ||||||
|  |             }) | ||||||
|  |         })?; | ||||||
|  |         let sketch_ids = sg.as_sketch_group_set(self.source_range); | ||||||
|  |         let extrude_ids = sg.as_extrude_group_set(self.source_range); | ||||||
|  |         let entity_ids = match (sketch_ids, extrude_ids) { | ||||||
|  |             (Ok(group), _) => group.ids(), | ||||||
|  |             (_, Ok(group)) => group.ids(), | ||||||
|  |             (Err(e), _) => return Err(e), | ||||||
|  |         }; | ||||||
|  |         Ok((num_repetitions, FnAsArg { func: transform, expr }, entity_ids)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fn get_segment_name_sketch_group(&self) -> Result<(String, Box<SketchGroup>), KclError> { |     fn get_segment_name_sketch_group(&self) -> Result<(String, Box<SketchGroup>), KclError> { | ||||||
|         // Iterate over our args, the first argument should be a UserVal with a string value. |         // Iterate over our args, the first argument should be a UserVal with a string value. | ||||||
|         // The second argument should be a SketchGroup. |         // The second argument should be a SketchGroup. | ||||||
| @ -437,19 +477,7 @@ impl Args { | |||||||
|             }) |             }) | ||||||
|         })?; |         })?; | ||||||
|  |  | ||||||
|         let sketch_set = if let MemoryItem::SketchGroup(sg) = first_value { |         let sketch_set = first_value.as_sketch_group_set(self.source_range)?; | ||||||
|             SketchGroupSet::SketchGroup(sg.clone()) |  | ||||||
|         } else if let MemoryItem::SketchGroups { value } = first_value { |  | ||||||
|             SketchGroupSet::SketchGroups(value.clone()) |  | ||||||
|         } else { |  | ||||||
|             return Err(KclError::Type(KclErrorDetails { |  | ||||||
|                 message: format!( |  | ||||||
|                     "Expected a SketchGroup or Vector of SketchGroups as the first argument, found `{:?}`", |  | ||||||
|                     self.args |  | ||||||
|                 ), |  | ||||||
|                 source_ranges: vec![self.source_range], |  | ||||||
|             })); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         let second_value = self.args.get(1).ok_or_else(|| { |         let second_value = self.args.get(1).ok_or_else(|| { | ||||||
|             KclError::Type(KclErrorDetails { |             KclError::Type(KclErrorDetails { | ||||||
| @ -672,19 +700,7 @@ impl Args { | |||||||
|             }) |             }) | ||||||
|         })?; |         })?; | ||||||
|  |  | ||||||
|         let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value { |         let sketch_set = second_value.as_sketch_group_set(self.source_range)?; | ||||||
|             SketchGroupSet::SketchGroup(sg.clone()) |  | ||||||
|         } else if let MemoryItem::SketchGroups { value } = second_value { |  | ||||||
|             SketchGroupSet::SketchGroups(value.clone()) |  | ||||||
|         } else { |  | ||||||
|             return Err(KclError::Type(KclErrorDetails { |  | ||||||
|                 message: format!( |  | ||||||
|                     "Expected a SketchGroup or Vector of SketchGroups as the second argument, found `{:?}`", |  | ||||||
|                     self.args |  | ||||||
|                 ), |  | ||||||
|                 source_ranges: vec![self.source_range], |  | ||||||
|             })); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         Ok((data, sketch_set)) |         Ok((data, sketch_set)) | ||||||
|     } |     } | ||||||
| @ -953,19 +969,7 @@ impl Args { | |||||||
|             }) |             }) | ||||||
|         })?; |         })?; | ||||||
|  |  | ||||||
|         let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value { |         let sketch_set = second_value.as_sketch_group_set(self.source_range)?; | ||||||
|             SketchGroupSet::SketchGroup(sg.clone()) |  | ||||||
|         } else if let MemoryItem::SketchGroups { value } = second_value { |  | ||||||
|             SketchGroupSet::SketchGroups(value.clone()) |  | ||||||
|         } else { |  | ||||||
|             return Err(KclError::Type(KclErrorDetails { |  | ||||||
|                 message: format!( |  | ||||||
|                     "Expected a SketchGroup or Vector of SketchGroups as the second argument, found `{:?}`", |  | ||||||
|                     self.args |  | ||||||
|                 ), |  | ||||||
|                 source_ranges: vec![self.source_range], |  | ||||||
|             })); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         Ok((number, sketch_set)) |         Ok((number, sketch_set)) | ||||||
|     } |     } | ||||||
| @ -1046,6 +1050,11 @@ pub enum Primitive { | |||||||
|     Uuid, |     Uuid, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | struct FnAsArg<'a> { | ||||||
|  |     pub func: &'a MemoryFunction, | ||||||
|  |     pub expr: Box<FunctionExpression>, | ||||||
|  | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use base64::Engine; |     use base64::Engine; | ||||||
|  | |||||||
| @ -5,13 +5,38 @@ use derive_docs::stdlib; | |||||||
| use kittycad::types::ModelingCmd; | use kittycad::types::ModelingCmd; | ||||||
| use schemars::JsonSchema; | use schemars::JsonSchema; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  | use uuid::Uuid; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     errors::{KclError, KclErrorDetails}, |     errors::{KclError, KclErrorDetails}, | ||||||
|     executor::{ExtrudeGroup, ExtrudeGroupSet, Geometries, Geometry, MemoryItem, SketchGroup, SketchGroupSet}, |     executor::{ | ||||||
|  |         ExtrudeGroup, ExtrudeGroupSet, FunctionParam, Geometries, Geometry, MemoryItem, Point3d, ProgramReturn, | ||||||
|  |         SketchGroup, SketchGroupSet, SourceRange, UserVal, | ||||||
|  |     }, | ||||||
|     std::{types::Uint, Args}, |     std::{types::Uint, Args}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const CANNOT_USE_ZERO_VECTOR: &str = | ||||||
|  |     "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."; | ||||||
|  |  | ||||||
|  | // /// How to change each element of a pattern. | ||||||
|  | // #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||||
|  | // #[ts(export)] | ||||||
|  | // #[serde(rename_all = "camelCase")] | ||||||
|  | // pub struct LinearTransform { | ||||||
|  | //     /// Translate the replica this far along each dimension. | ||||||
|  | //     /// Defaults to zero vector (i.e. same position as the original). | ||||||
|  | //     #[serde(default)] | ||||||
|  | //     pub translate: Option<Point3d>, | ||||||
|  | //     /// Scale the replica's size along each axis. | ||||||
|  | //     /// Defaults to (1, 1, 1) (i.e. the same size as the original). | ||||||
|  | //     #[serde(default)] | ||||||
|  | //     pub scale: Option<Point3d>, | ||||||
|  | //     /// Whether to replicate the original solid in this instance. | ||||||
|  | //     #[serde(default)] | ||||||
|  | //     pub replicate: Option<bool>, | ||||||
|  | // } | ||||||
|  |  | ||||||
| /// Data for a linear pattern on a 2D sketch. | /// Data for a linear pattern on a 2D sketch. | ||||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||||
| #[ts(export)] | #[ts(export)] | ||||||
| @ -70,15 +95,35 @@ impl LinearPattern { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// A linear pattern, either 2D or 3D. | ||||||
|  | /// Each element in the pattern repeats a particular piece of geometry. | ||||||
|  | /// The repetitions can be transformed by the `transform` parameter. | ||||||
|  | pub async fn pattern(args: Args) -> Result<MemoryItem, KclError> { | ||||||
|  |     let (num_repetitions, transform, entity_ids) = args.get_pattern_args()?; | ||||||
|  |  | ||||||
|  |     let sketch_groups = inner_pattern( | ||||||
|  |         num_repetitions, | ||||||
|  |         FunctionParam { | ||||||
|  |             inner: transform.func, | ||||||
|  |             fn_expr: transform.expr, | ||||||
|  |             meta: vec![args.source_range.into()], | ||||||
|  |             ctx: args.ctx.clone(), | ||||||
|  |             memory: args.memory.clone(), | ||||||
|  |         }, | ||||||
|  |         entity_ids, | ||||||
|  |         &args, | ||||||
|  |     ) | ||||||
|  |     .await?; | ||||||
|  |     Ok(MemoryItem::SketchGroups { value: sketch_groups }) | ||||||
|  | } | ||||||
|  |  | ||||||
| /// A linear pattern on a 2D sketch. | /// A linear pattern on a 2D sketch. | ||||||
| pub async fn pattern_linear_2d(args: Args) -> Result<MemoryItem, KclError> { | pub async fn pattern_linear_2d(args: Args) -> Result<MemoryItem, KclError> { | ||||||
|     let (data, sketch_group_set): (LinearPattern2dData, SketchGroupSet) = args.get_data_and_sketch_group_set()?; |     let (data, sketch_group_set): (LinearPattern2dData, SketchGroupSet) = args.get_data_and_sketch_group_set()?; | ||||||
|  |  | ||||||
|     if data.axis == [0.0, 0.0] { |     if data.axis == [0.0, 0.0] { | ||||||
|         return Err(KclError::Semantic(KclErrorDetails { |         return Err(KclError::Semantic(KclErrorDetails { | ||||||
|             message: |             message: CANNOT_USE_ZERO_VECTOR.to_string(), | ||||||
|                 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place." |  | ||||||
|                     .to_string(), |  | ||||||
|             source_ranges: vec![args.source_range], |             source_ranges: vec![args.source_range], | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
| @ -87,6 +132,106 @@ pub async fn pattern_linear_2d(args: Args) -> Result<MemoryItem, KclError> { | |||||||
|     Ok(MemoryItem::SketchGroups { value: sketch_groups }) |     Ok(MemoryItem::SketchGroups { value: sketch_groups }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// A linear pattern on a 2D or 3D solid. | ||||||
|  | /// Each repetition of the pattern can be transformed (e.g. scaled, translated, hidden, etc). | ||||||
|  | /// | ||||||
|  | /// ```no_run | ||||||
|  | /// The vase is 100 layers tall. | ||||||
|  | /// The 100 layers are replica of each other, with a slight transformation applied to each. | ||||||
|  | /// let vase = layer() |> pattern(100, transform, %) | ||||||
|  | /// // base radius | ||||||
|  | /// const r = 50 | ||||||
|  | /// // layer height | ||||||
|  | /// const h = 10 | ||||||
|  | /// // taper factor [0 - 1) | ||||||
|  | /// const t = 0.005 | ||||||
|  | /// // Each layer is just a pretty thin cylinder. | ||||||
|  | /// fn layer = () => { | ||||||
|  | ///   return startSketchOn("XY") // or some other plane idk | ||||||
|  | ///     |> circle([0, 0], 1, %) | ||||||
|  | ///     |> extrude(h, %) | ||||||
|  | /// // Change each replica's radius and shift it up the Z axis. | ||||||
|  | /// fn transform = (replicaId) => { | ||||||
|  | ///   return { | ||||||
|  | ///     translate: [0, 0, replicaId*10] | ||||||
|  | ///     scale: r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8)) | ||||||
|  | ///   } | ||||||
|  | /// } | ||||||
|  | /// ``` | ||||||
|  | #[stdlib { | ||||||
|  |     name = "pattern", | ||||||
|  | }] | ||||||
|  | async fn inner_pattern<'a>( | ||||||
|  |     num_repetitions: u32, | ||||||
|  |     transform_function: FunctionParam<'a>, | ||||||
|  |     ids: Vec<Uuid>, | ||||||
|  |     args: &'a Args, | ||||||
|  | ) -> Result<Vec<Box<SketchGroup>>, KclError> { | ||||||
|  |     // Build the vec of transforms, one for each repetition. | ||||||
|  |     let mut transforms = Vec::new(); | ||||||
|  |     for i in 0..num_repetitions { | ||||||
|  |         // Call the transform fn for this repetition. | ||||||
|  |         let repetition_num = MemoryItem::UserVal(UserVal { | ||||||
|  |             value: serde_json::Value::Number(i.into()), | ||||||
|  |             meta: vec![args.source_range.into()], | ||||||
|  |         }); | ||||||
|  |         let transform_fn_args = vec![repetition_num]; | ||||||
|  |         let transform_fn_return = transform_function.call(transform_fn_args).await?; | ||||||
|  |  | ||||||
|  |         // Unpack the returned transform object. | ||||||
|  |         let transform_fn_return = transform_fn_return.ok_or_else(|| { | ||||||
|  |             KclError::Semantic(KclErrorDetails { | ||||||
|  |                 message: "Transform function must return a value".to_string(), | ||||||
|  |                 source_ranges: vec![args.source_range], | ||||||
|  |             }) | ||||||
|  |         })?; | ||||||
|  |         let ProgramReturn::Value(transform_fn_return) = transform_fn_return else { | ||||||
|  |             return Err(KclError::Semantic(KclErrorDetails { | ||||||
|  |                 message: "Transform function must return a value".to_string(), | ||||||
|  |                 source_ranges: vec![args.source_range], | ||||||
|  |             })); | ||||||
|  |         }; | ||||||
|  |         let MemoryItem::UserVal(transform) = transform_fn_return else { | ||||||
|  |             return Err(KclError::Semantic(KclErrorDetails { | ||||||
|  |                 message: "Transform function must return a transform object".to_string(), | ||||||
|  |                 source_ranges: vec![args.source_range], | ||||||
|  |             })); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // Apply defaults to the transform. | ||||||
|  |         let replicate = match transform.value.get("replicate") { | ||||||
|  |             Some(serde_json::Value::Bool(true)) => true, | ||||||
|  |             Some(serde_json::Value::Bool(false)) => false, | ||||||
|  |             Some(_) => { | ||||||
|  |                 return Err(KclError::Semantic(KclErrorDetails { | ||||||
|  |                     message: "The 'replicate' key must be a bool".to_string(), | ||||||
|  |                     source_ranges: vec![args.source_range], | ||||||
|  |                 })); | ||||||
|  |             } | ||||||
|  |             None => true, | ||||||
|  |         }; | ||||||
|  |         let scale = match transform.value.get("scale") { | ||||||
|  |             Some(x) => array_to_point3d(x, vec![args.source_range])?, | ||||||
|  |             None => Point3d { x: 1.0, y: 1.0, z: 1.0 }, | ||||||
|  |         }; | ||||||
|  |         let translate = match transform.value.get("translate") { | ||||||
|  |             Some(x) => array_to_point3d(x, vec![args.source_range])?, | ||||||
|  |             None => Point3d { x: 0.0, y: 0.0, z: 0.0 }, | ||||||
|  |         }; | ||||||
|  |         let t = kittycad::types::LinearTransform { | ||||||
|  |             replicate, | ||||||
|  |             scale: Some(scale.into()), | ||||||
|  |             translate: Some(translate.into()), | ||||||
|  |         }; | ||||||
|  |         transforms.push(dbg!(t)); | ||||||
|  |     } | ||||||
|  |     for id in ids { | ||||||
|  |         // Call the pattern API endpoint. | ||||||
|  |         send_pattern_cmd(id, transforms.clone(), args).await?; | ||||||
|  |     } | ||||||
|  |     Ok(Vec::new()) | ||||||
|  | } | ||||||
|  |  | ||||||
| /// A linear pattern on a 2D sketch. | /// A linear pattern on a 2D sketch. | ||||||
| /// | /// | ||||||
| /// ```no_run | /// ```no_run | ||||||
| @ -212,6 +357,27 @@ async fn inner_pattern_linear_3d( | |||||||
|     Ok(extrude_groups) |     Ok(extrude_groups) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async fn send_pattern_cmd( | ||||||
|  |     entity_id: Uuid, | ||||||
|  |     transform: Vec<kittycad::types::LinearTransform>, | ||||||
|  |     args: &Args, | ||||||
|  | ) -> Result<kittycad::types::EntityLinearPatternTransform, KclError> { | ||||||
|  |     let id = uuid::Uuid::new_v4(); | ||||||
|  |     let resp = args | ||||||
|  |         .send_modeling_cmd(id, ModelingCmd::EntityLinearPatternTransform { entity_id, transform }) | ||||||
|  |         .await?; | ||||||
|  |     let kittycad::types::OkWebSocketResponseData::Modeling { | ||||||
|  |         modeling_response: kittycad::types::OkModelingCmdResponse::EntityLinearPatternTransform { data: pattern_info }, | ||||||
|  |     } = &resp | ||||||
|  |     else { | ||||||
|  |         return Err(KclError::Engine(KclErrorDetails { | ||||||
|  |             message: format!("EntityLinearPatternTransform response was not as expected: {:?}", resp), | ||||||
|  |             source_ranges: vec![args.source_range], | ||||||
|  |         })); | ||||||
|  |     }; | ||||||
|  |     Ok(pattern_info.to_owned()) | ||||||
|  | } | ||||||
|  |  | ||||||
| async fn pattern_linear(data: LinearPattern, geometry: Geometry, args: Args) -> Result<Geometries, KclError> { | async fn pattern_linear(data: LinearPattern, geometry: Geometry, args: Args) -> Result<Geometries, KclError> { | ||||||
|     let id = uuid::Uuid::new_v4(); |     let id = uuid::Uuid::new_v4(); | ||||||
|  |  | ||||||
| @ -524,3 +690,31 @@ async fn pattern_circular(data: CircularPattern, geometry: Geometry, args: Args) | |||||||
|  |  | ||||||
|     Ok(geometries) |     Ok(geometries) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fn array_to_point3d(json: &serde_json::Value, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> { | ||||||
|  |     let serde_json::Value::Array(arr) = dbg!(json) else { | ||||||
|  |         return Err(KclError::Semantic(KclErrorDetails { | ||||||
|  |             message: "Expected an array of 3 numbers (i.e. a 3D point)".to_string(), | ||||||
|  |             source_ranges, | ||||||
|  |         })); | ||||||
|  |     }; | ||||||
|  |     let len = arr.len(); | ||||||
|  |     if len != 3 { | ||||||
|  |         return Err(KclError::Semantic(KclErrorDetails { | ||||||
|  |             message: format!("Expected an array of 3 numbers (i.e. a 3D point) but found {len} items"), | ||||||
|  |             source_ranges, | ||||||
|  |         })); | ||||||
|  |     }; | ||||||
|  |     // Gets an f64 from a JSON value, returns Option. | ||||||
|  |     let f = |j: &serde_json::Value| j.as_number().and_then(|num| num.as_f64()).map(|x| x.to_owned()); | ||||||
|  |     let err = |component| { | ||||||
|  |         KclError::Semantic(KclErrorDetails { | ||||||
|  |             message: format!("{component} component of this point was not a number"), | ||||||
|  |             source_ranges: source_ranges.clone(), | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |     let x = f(&arr[0]).ok_or_else(|| err("X"))?; | ||||||
|  |     let y = f(&arr[1]).ok_or_else(|| err("Y"))?; | ||||||
|  |     let z = f(&arr[2]).ok_or_else(|| err("Z"))?; | ||||||
|  |     Ok(Point3d { x, y, z }) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								src/wasm-lib/tests/executor/inputs/pattern_vase.kcl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/wasm-lib/tests/executor/inputs/pattern_vase.kcl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | // Defines a vase. | ||||||
|  | // The vase is made of 100 layers. | ||||||
|  |  | ||||||
|  | // Parameters | ||||||
|  | const r = 50    // base radius | ||||||
|  | const h = 10    // layer height | ||||||
|  | const t = 0.005 // taper factor [0-1) | ||||||
|  |  | ||||||
|  | // Defines how to modify each layer of the vase. | ||||||
|  | // Each replica is shifted up the Z axis, and has a smoothly-varying radius | ||||||
|  | fn transform = (replicaId) => { | ||||||
|  |   let scale = r * abs(1 - (t * replicaId)) * (5 + cos(replicaId / 8)) | ||||||
|  |   return { | ||||||
|  |     translate: [0, 0, replicaId * 10], | ||||||
|  |     scale: [scale, scale, 0], | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Each layer is just a pretty thin cylinder with a fillet. | ||||||
|  | fn layer = () => { | ||||||
|  |   return startSketchOn("XY") // or some other plane idk | ||||||
|  |     |> circle([0, 0], 1, %, 'tag1') | ||||||
|  |     |> extrude(h, %) | ||||||
|  |     // |> fillet({ | ||||||
|  |     //        radius: h / 2.01, | ||||||
|  |     //        tags: ["tag1", getOppositeEdge("tag1", %)] | ||||||
|  |     //    }, %) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // The vase is 100 layers tall. | ||||||
|  | // The 100 layers are replica of each other, with a slight transformation applied to each. | ||||||
|  | let vase = layer() |> pattern(100, transform, %) | ||||||
| @ -92,6 +92,13 @@ async fn serial_test_riddle_small() { | |||||||
|     twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, 0.999); |     twenty_twenty::assert_image("tests/executor/outputs/riddle_small.png", &result, 0.999); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[tokio::test(flavor = "multi_thread")] | ||||||
|  | async fn serial_test_pattern_vase() { | ||||||
|  |     let code = include_str!("inputs/pattern_vase.kcl"); | ||||||
|  |     let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap(); | ||||||
|  |     twenty_twenty::assert_image("tests/executor/outputs/pattern_vase.png", &result, 0.999); | ||||||
|  | } | ||||||
|  |  | ||||||
| #[tokio::test(flavor = "multi_thread")] | #[tokio::test(flavor = "multi_thread")] | ||||||
| async fn serial_test_lego() { | async fn serial_test_lego() { | ||||||
|     let code = include_str!("inputs/lego.kcl"); |     let code = include_str!("inputs/lego.kcl"); | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/wasm-lib/tests/executor/outputs/pattern_vase.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/wasm-lib/tests/executor/outputs/pattern_vase.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 333 KiB | 
		Reference in New Issue
	
	Block a user
	