Compare commits
7 Commits
jtran/sket
...
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