Compare commits
	
		
			7 Commits
		
	
	
		
			franknoiro
			...
			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]] | ||||
| name = "kittycad" | ||||
| version = "0.3.5" | ||||
| version = "0.3.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0" | ||||
| checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "async-trait", | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1456,9 +1456,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "kittycad" | ||||
| version = "0.3.5" | ||||
| version = "0.3.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "df75feef10313fa1cb15b7cecd0f579877312ba3d42bb5b8b4c1d4b1d0fcabf0" | ||||
| checksum = "af3de9bb4b1441f198689a9f64a8163a518377e30b348a784680e738985b95eb" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "async-trait", | ||||
|  | ||||
| @ -69,7 +69,7 @@ members = [ | ||||
| ] | ||||
|  | ||||
| [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" | ||||
|  | ||||
| [[test]] | ||||
|  | ||||
| @ -96,10 +96,16 @@ fn do_stdlib_inner( | ||||
|     } | ||||
|  | ||||
|     if !ast.sig.generics.params.is_empty() { | ||||
|         errors.push(Error::new_spanned( | ||||
|             &ast.sig.generics, | ||||
|             "generics are not permitted for stdlib functions", | ||||
|         )); | ||||
|         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( | ||||
|                 &ast.sig.generics, | ||||
|                 "Stdlib functions may not be generic over types or constants, only lifetimes.", | ||||
|             )); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if ast.sig.variadic.is_some() { | ||||
| @ -650,7 +656,12 @@ impl Parse for ItemFnForSignature { | ||||
| } | ||||
|  | ||||
| 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") { | ||||
|         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] | ||||
| fn test_stdlib_line_to() { | ||||
|     let (item, errors) = do_stdlib( | ||||
| @ -64,7 +114,6 @@ fn test_stdlib_line_to() { | ||||
|         }, | ||||
|     ) | ||||
|     .unwrap(); | ||||
|     let _expected = quote! {}; | ||||
|  | ||||
|     assert!(errors.is_empty()); | ||||
|     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) { | ||||
|             FunctionKind::Core(func) => { | ||||
|                 // 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?; | ||||
|                 Ok(result) | ||||
|             } | ||||
|  | ||||
| @ -189,6 +189,15 @@ pub enum SketchGroupSet { | ||||
|     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. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| @ -198,6 +207,15 @@ pub enum ExtrudeGroupSet { | ||||
|     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. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[ts(export)] | ||||
| @ -298,6 +316,39 @@ pub struct UserVal { | ||||
|     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 = | ||||
|     fn( | ||||
|         s: Vec<MemoryItem>, | ||||
| @ -413,6 +464,88 @@ impl MemoryItem { | ||||
|         }; | ||||
|         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. | ||||
| @ -1193,7 +1326,7 @@ impl ExecutorContext { | ||||
|                         } | ||||
|                         match self.stdlib.get_either(&call_expr.callee.name) { | ||||
|                             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?; | ||||
|                                 memory.return_ = Some(ProgramReturn::Value(result)); | ||||
|                             } | ||||
|  | ||||
| @ -25,14 +25,15 @@ use lazy_static::lazy_static; | ||||
| use parse_display::{Display, FromStr}; | ||||
| use schemars::JsonSchema; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| use crate::{ | ||||
|     ast::types::parse_json_number_as_f64, | ||||
|     ast::types::{parse_json_number_as_f64, FunctionExpression}, | ||||
|     docs::StdLibFn, | ||||
|     errors::{KclError, KclErrorDetails}, | ||||
|     executor::{ | ||||
|         ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryItem, Metadata, SketchGroup, SketchGroupSet, | ||||
|         SketchSurface, SourceRange, | ||||
|         ExecutorContext, ExtrudeGroup, ExtrudeGroupSet, MemoryFunction, MemoryItem, Metadata, ProgramMemory, | ||||
|         SketchGroup, SketchGroupSet, SketchSurface, SourceRange, | ||||
|     }, | ||||
|     std::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag}, | ||||
| }; | ||||
| @ -84,6 +85,7 @@ lazy_static! { | ||||
|         Box::new(crate::std::patterns::PatternLinear3D), | ||||
|         Box::new(crate::std::patterns::PatternCircular2D), | ||||
|         Box::new(crate::std::patterns::PatternCircular3D), | ||||
|         Box::new(crate::std::patterns::Pattern), | ||||
|         Box::new(crate::std::chamfer::Chamfer), | ||||
|         Box::new(crate::std::fillet::Fillet), | ||||
|         Box::new(crate::std::fillet::GetOppositeEdge), | ||||
| @ -204,14 +206,17 @@ pub struct Args { | ||||
|     pub args: Vec<MemoryItem>, | ||||
|     pub source_range: SourceRange, | ||||
|     pub ctx: ExecutorContext, | ||||
|     // TODO: This should be reference, not clone. | ||||
|     pub memory: ProgramMemory, | ||||
| } | ||||
|  | ||||
| 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 { | ||||
|             args, | ||||
|             source_range, | ||||
|             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> { | ||||
|         // Iterate over our args, the first argument should be a UserVal with a string value. | ||||
|         // The second argument should be a SketchGroup. | ||||
| @ -437,19 +477,7 @@ impl Args { | ||||
|             }) | ||||
|         })?; | ||||
|  | ||||
|         let sketch_set = if let MemoryItem::SketchGroup(sg) = first_value { | ||||
|             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 sketch_set = first_value.as_sketch_group_set(self.source_range)?; | ||||
|  | ||||
|         let second_value = self.args.get(1).ok_or_else(|| { | ||||
|             KclError::Type(KclErrorDetails { | ||||
| @ -672,19 +700,7 @@ impl Args { | ||||
|             }) | ||||
|         })?; | ||||
|  | ||||
|         let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value { | ||||
|             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], | ||||
|             })); | ||||
|         }; | ||||
|         let sketch_set = second_value.as_sketch_group_set(self.source_range)?; | ||||
|  | ||||
|         Ok((data, sketch_set)) | ||||
|     } | ||||
| @ -953,19 +969,7 @@ impl Args { | ||||
|             }) | ||||
|         })?; | ||||
|  | ||||
|         let sketch_set = if let MemoryItem::SketchGroup(sg) = second_value { | ||||
|             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], | ||||
|             })); | ||||
|         }; | ||||
|         let sketch_set = second_value.as_sketch_group_set(self.source_range)?; | ||||
|  | ||||
|         Ok((number, sketch_set)) | ||||
|     } | ||||
| @ -1046,6 +1050,11 @@ pub enum Primitive { | ||||
|     Uuid, | ||||
| } | ||||
|  | ||||
| struct FnAsArg<'a> { | ||||
|     pub func: &'a MemoryFunction, | ||||
|     pub expr: Box<FunctionExpression>, | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use base64::Engine; | ||||
|  | ||||
| @ -5,13 +5,38 @@ use derive_docs::stdlib; | ||||
| use kittycad::types::ModelingCmd; | ||||
| use schemars::JsonSchema; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| use crate::{ | ||||
|     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}, | ||||
| }; | ||||
|  | ||||
| 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. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| #[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. | ||||
| 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()?; | ||||
|  | ||||
|     if data.axis == [0.0, 0.0] { | ||||
|         return Err(KclError::Semantic(KclErrorDetails { | ||||
|             message: | ||||
|                 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place." | ||||
|                     .to_string(), | ||||
|             message: CANNOT_USE_ZERO_VECTOR.to_string(), | ||||
|             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 }) | ||||
| } | ||||
|  | ||||
| /// 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. | ||||
| /// | ||||
| /// ```no_run | ||||
| @ -212,6 +357,27 @@ async fn inner_pattern_linear_3d( | ||||
|     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> { | ||||
|     let id = uuid::Uuid::new_v4(); | ||||
|  | ||||
| @ -524,3 +690,31 @@ async fn pattern_circular(data: CircularPattern, geometry: Geometry, args: Args) | ||||
|  | ||||
|     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); | ||||
| } | ||||
|  | ||||
| #[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")] | ||||
| async fn serial_test_lego() { | ||||
|     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
	