From ed7abf7d96cf74faa4d8d79faa58f4395cf0ad58 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Wed, 25 May 2022 23:51:11 +0200 Subject: [PATCH 1/8] Create procedural macro to remove some boilerplate With this change the `model` function is a lot nicer to write default values are provided as macro arguments currently all arguments **must** have a default value defined this way there are also no checks in place to ensure the default is valid for the type of the argument Signed-off-by: gabsi26 --- crates/fj-proc/Cargo.toml | 22 +++++++++++++ crates/fj-proc/src/lib.rs | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 crates/fj-proc/Cargo.toml create mode 100644 crates/fj-proc/src/lib.rs diff --git a/crates/fj-proc/Cargo.toml b/crates/fj-proc/Cargo.toml new file mode 100644 index 000000000..1df5a8d98 --- /dev/null +++ b/crates/fj-proc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "fj-proc" +version = "0.6.0" +edition = "2021" + +description = "The world needs another CAD program." +readme = "../../README.md" +homepage = "https://www.fornjot.app/" +repository = "https://github.com/hannobraun/fornjot" +license = "0BSD" +keywords = ["cad", "programmatic", "code-cad"] +categories = ["encoding", "mathematics", "rendering"] + + +[lib] +proc-macro = true + +[dependencies] +serde = { version = "1.0.7", optional = true } +syn={version="1.0",features=["full", "extra-traits"]} +quote = "1.0" +proc-macro2 = "1.0" diff --git a/crates/fj-proc/src/lib.rs b/crates/fj-proc/src/lib.rs new file mode 100644 index 000000000..d49bb9f7c --- /dev/null +++ b/crates/fj-proc/src/lib.rs @@ -0,0 +1,66 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +#[proc_macro_attribute] +pub fn model(default_values: TokenStream, input: TokenStream) -> TokenStream { + let vals: Vec = default_values + .into_iter() + .filter_map(|tree| { + if let proc_macro::TokenTree::Literal(lit) = tree { + Some(lit.to_string()) + } else { + None + } + }) + .collect(); + let item = parse_macro_input!(input as syn::ItemFn); + + let inputs = item.sig.inputs; + let mut names = Vec::new(); + let mut types = Vec::new(); + for f in inputs.iter() { + if let syn::FnArg::Typed(meep) = f { + if let syn::Pat::Ident(ident) = *meep.clone().pat { + names.push(ident.ident); + } + if let syn::Type::Path(path) = *meep.clone().ty { + types.push(path.path.get_ident().unwrap().clone()); + } + } + } + let block = item.block; + + quote! { + #[no_mangle] + pub extern "C" fn model(args: &HashMap) -> fj::Shape { + #(let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#vals.parse::<#types>().unwrap());)* + #block + } + }.into() +} + +// #[fj_proc::model(5, 1.0, 2.0, 1.0)] +// pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape { +// } + +// #[no_mangle] +// pub extern "C" fn model(args: &HashMap) -> fj::Shape { +// let num_points: u64 = args +// .get("num_points") +// .map(|arg| arg.parse().unwrap()) +// .unwrap_or(5); + +// let r1: f64 = args +// .get("r1") +// .map(|arg| arg.parse().unwrap()) +// .unwrap_or(1.0); + +// let r2: f64 = args +// .get("r2") +// .map(|arg| arg.parse().unwrap()) +// .unwrap_or(2.0); + +// let h: f64 = args.get("h").map(|arg| arg.parse().unwrap()).unwrap_or(1.0); + +// } From 1b623b351cfe768137479a7ab79b020c3af12cb2 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Wed, 25 May 2022 23:52:09 +0200 Subject: [PATCH 2/8] Include fj-proc in workspace Signed-off-by: gabsi26 --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 192258230..7bea513eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,12 @@ members = [ "crates/fj-math", "crates/fj-operations", "crates/fj-viewer", + "crates/fj-window", + "crates/fj-proc", + + "models/cuboid", "models/spacer", "models/star", From 29dbeb547266f647870cc787548c0fd02fd49d41 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Wed, 25 May 2022 23:52:34 +0200 Subject: [PATCH 3/8] Update star model to use the procedural macro Signed-off-by: gabsi26 --- models/star/Cargo.toml | 3 +++ models/star/src/lib.rs | 37 ++++--------------------------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/models/star/Cargo.toml b/models/star/Cargo.toml index c5c2d4efd..658366623 100644 --- a/models/star/Cargo.toml +++ b/models/star/Cargo.toml @@ -8,3 +8,6 @@ crate-type = ["cdylib"] [dependencies.fj] path = "../../crates/fj" + +[dependencies.fj-proc] +path = "../../crates/fj-proc" \ No newline at end of file diff --git a/models/star/src/lib.rs b/models/star/src/lib.rs index ba2003b37..cb8691be1 100644 --- a/models/star/src/lib.rs +++ b/models/star/src/lib.rs @@ -1,38 +1,9 @@ use fj::Angle; use std::{collections::HashMap, f64::consts::PI}; +extern crate fj_proc; -#[no_mangle] -pub extern "C" fn model(args: &HashMap) -> fj::Shape { - // Number of points of the star - // - // "Points" in the sense of "pointy ends", not in the sense of geometrical - // points, or vertices. - let num_points: u64 = args - .get("num_points") - .map(|arg| arg.parse().expect("Could not parse parameter `num_points`")) - .unwrap_or(5); - - // Radius of the circle that all the vertices between the pointy ends are on - let r1: f64 = args - .get("r1") - .map(|arg| arg.parse().expect("Could not parse parameter `r1`")) - .unwrap_or(1.0); - - // Radius of the circle that all the pointy ends are on - let r2: f64 = args - .get("r2") - .map(|arg| arg.parse().expect("Could not parse parameter `r2`")) - .unwrap_or(2.0); - - // The height of the star - let h: f64 = args - .get("h") - .map(|arg| arg.parse().expect("Could not parse parameter `height`")) - .unwrap_or(1.0); - - // We need to figure out where to generate vertices, depending on the number - // of points the star is supposed to have. Let's generate an iterator that - // gives us the angle and radius for each vertex. +#[fj_proc::model(5, 1.0, 2.0, 1.0)] +pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape { let num_vertices = num_points * 2; let vertex_iter = (0..num_vertices).map(|i| { let angle = Angle::from_rad(2. * PI / num_vertices as f64 * i as f64); @@ -59,7 +30,7 @@ pub extern "C" fn model(args: &HashMap) -> fj::Shape { let footprint = fj::Difference2d::from_shapes([outer.into(), inner.into()]); - let star = fj::Sweep::from_path(footprint.into(), [0., 0., h]); + let star = fj::Sweep::from_path(footprint.into(), [0., 0., -h]); star.into() } From 79ae0239b88ae897e3924d1a910c41d32969ae2e Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Wed, 25 May 2022 23:52:09 +0200 Subject: [PATCH 4/8] Include fj-proc in workspace Signed-off-by: gabsi26 --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 7bea513eb..f227d7790 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/fj-math", "crates/fj-operations", "crates/fj-viewer", + "crates/fj-proc", "crates/fj-window", From 495dcfe90491c9de04531d09d608f5313fdeb0b3 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Sun, 29 May 2022 16:04:52 +0200 Subject: [PATCH 5/8] Create `attributed_arguments` variant proc_macro This is a first iteration on a asuggested way to implement modelling convenience Signed-off-by: gabsi26 --- crates/fj-proc/src/attributed_arguments.rs | 212 +++++++++++++++++++++ crates/fj-proc/src/lib.rs | 10 + 2 files changed, 222 insertions(+) create mode 100644 crates/fj-proc/src/attributed_arguments.rs diff --git a/crates/fj-proc/src/attributed_arguments.rs b/crates/fj-proc/src/attributed_arguments.rs new file mode 100644 index 000000000..e09e01de4 --- /dev/null +++ b/crates/fj-proc/src/attributed_arguments.rs @@ -0,0 +1,212 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ + bracketed, parenthesized, parse::Parse, parse_macro_input, parse_quote, +}; + +pub fn attributed_arguments( + _default_values: TokenStream, + input: TokenStream, +) -> TokenStream { + let item = parse_macro_input!(input as syn::ItemFn); + let inputs = item.clone().sig.inputs; + let args: Vec = + inputs.iter().map(|inp| parse_quote!(#inp)).collect(); + let mut defaults = Vec::new(); + let mut mins = Vec::new(); + let mut maxs = Vec::new(); + let mut names = Vec::new(); + let mut types = Vec::new(); + for arg in args { + let mut default = None; + let mut min = None; + let mut max = None; + names.push(arg.ident); + types.push(arg.ty); + for value in arg.attr.values.clone() { + if let Some(ident) = value.ident.clone() { + match ident.to_string().as_str() { + "default" => default = Some(value.val.clone()), + "min" => min = Some(value.val.clone()), + "max" => max = Some(value.val.clone()), + _ => {} + } + } else { + default = Some(value.val.clone()); + } + } + let attr_param = AttributeParameter::new(default, min, max); + defaults.push(attr_param.default); + mins.push(attr_param.min); + maxs.push(attr_param.max); + } + let block = item.block; + println!( + "{}", + quote! { + #[no_mangle] + pub extern "C" fn model(args: &HashMap) -> fj::Shape { + #( + let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#defaults); + )* + #block + } + } + ); + quote! { + #[no_mangle] + pub extern "C" fn model(args: &HashMap) -> fj::Shape { + #( + let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#defaults); + )* + #block + } + }.into() +} + +#[derive(Debug, Clone)] +struct Argument { + pub attr: HelperAttribute, + pub ident: proc_macro2::Ident, + pub ty: proc_macro2::Ident, +} + +impl Parse for Argument { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let attr: HelperAttribute = input.parse()?; + let ident: proc_macro2::Ident = input.parse()?; + + let _: syn::token::Colon = input.parse()?; + let ty: proc_macro2::Ident = input.parse()?; + println!("attr: {:?}, ident: {:?}, ty: {:?}\n", attr, ident, ty); + Ok(Self { attr, ident, ty }) + } +} + +#[derive(Debug, Clone)] +struct HelperAttribute { + pub values: syn::punctuated::Punctuated, +} + +impl Parse for HelperAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let attr_content; + let value_content; + let _: syn::token::Pound = input.parse()?; + bracketed!(attr_content in input); + let ident: proc_macro2::Ident = attr_content.parse()?; + if ident.to_string() != *"value" { + return Err(syn::Error::new_spanned( + ident.clone(), + format!( + "Unknown attribute \"{}\" found, expected \"value\"", + ident + ), + )); + } + parenthesized!(value_content in attr_content); + + Ok(Self { + values: syn::punctuated::Punctuated::parse_separated_nonempty_with( + &value_content, + DefaultParam::parse, + )?, + }) + } +} + +#[derive(Debug, Clone)] +struct DefaultParam { + pub ident: Option, + pub val: syn::Expr, +} + +impl Parse for DefaultParam { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.peek(syn::Ident) { + let ident: Option = Some(input.parse()?); + let _: syn::token::Eq = input.parse()?; + Ok(Self { + ident, + val: input.parse()?, + }) + } else { + Ok(Self { + ident: None, + val: input.parse()?, + }) + } + } +} + +#[derive(Debug)] +struct AttributeParameter { + pub default: Option, + pub min: Option, + pub max: Option, +} + +impl AttributeParameter { + // TODO: Checking the Options is quite ugly + pub fn new( + default: Option, + min: Option, + max: Option, + ) -> Self { + if let Some(default) = default { + let min = if min.is_some() { min } else { None }; + let max = if max.is_some() { max } else { None }; + Self { + default: Some(default), + min, + max, + } + } else { + let mut default = None; + let max = if max.is_some() { + default = max.clone(); + max + } else { + None + }; + + let min = if min.is_some() { + default = min.clone(); + min + } else { + None + }; + + Self { default, min, max } + } + } +} + +// #[fj::model] +// pub fn model( +// #[default(5)] num_points: u64, +// #[default(1.0)] r1: f64, +// #[default(2.0)] r2: f64, +// #[default(1.0)] h: f64, +// ) -> fj::Shape + +// #[no_mangle] +// pub extern "C" fn model(args: &HashMap) -> fj::Shape { +// let num_points: u64 = args +// .get("num_points") +// .map(|arg| arg.parse().unwrap()) +// .unwrap_or(5); + +// let r1: f64 = args +// .get("r1") +// .map(|arg| arg.parse().unwrap()) +// .unwrap_or(1.0); + +// let r2: f64 = args +// .get("r2") +// .map(|arg| arg.parse().unwrap()) +// .unwrap_or(2.0); + +// let h: f64 = args.get("h").map(|arg| arg.parse().unwrap()).unwrap_or(1.0); + +// } diff --git a/crates/fj-proc/src/lib.rs b/crates/fj-proc/src/lib.rs index d49bb9f7c..eebdaf37b 100644 --- a/crates/fj-proc/src/lib.rs +++ b/crates/fj-proc/src/lib.rs @@ -2,6 +2,8 @@ use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; +mod attributed_arguments; + #[proc_macro_attribute] pub fn model(default_values: TokenStream, input: TokenStream) -> TokenStream { let vals: Vec = default_values @@ -40,6 +42,14 @@ pub fn model(default_values: TokenStream, input: TokenStream) -> TokenStream { }.into() } +#[proc_macro_attribute] +pub fn attributed_arguments( + default_values: TokenStream, + input: TokenStream, +) -> TokenStream { + attributed_arguments::attributed_arguments(default_values, input) +} + // #[fj_proc::model(5, 1.0, 2.0, 1.0)] // pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape { // } From 934fca268278ff3498152cbc18dacdefaafa59e9 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Sun, 29 May 2022 16:05:26 +0200 Subject: [PATCH 6/8] Use `attributed_arguments` macro in an example Signed-off-by: gabsi26 --- Cargo.toml | 1 + models/star_attributed_arguments/Cargo.toml | 13 +++++++ models/star_attributed_arguments/src/lib.rs | 41 +++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 models/star_attributed_arguments/Cargo.toml create mode 100644 models/star_attributed_arguments/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index f227d7790..274f300e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "models/cuboid", "models/spacer", "models/star", + "models/star_attributed_arguments", "models/test", "tools/export-validator", diff --git a/models/star_attributed_arguments/Cargo.toml b/models/star_attributed_arguments/Cargo.toml new file mode 100644 index 000000000..a22353e9a --- /dev/null +++ b/models/star_attributed_arguments/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "star_attributed_arguments" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies.fj] +path = "../../crates/fj" + +[dependencies.fj-proc] +path = "../../crates/fj-proc" \ No newline at end of file diff --git a/models/star_attributed_arguments/src/lib.rs b/models/star_attributed_arguments/src/lib.rs new file mode 100644 index 000000000..5a4364b32 --- /dev/null +++ b/models/star_attributed_arguments/src/lib.rs @@ -0,0 +1,41 @@ +use fj::Angle; +use std::{collections::HashMap, f64::consts::PI}; +extern crate fj_proc; + +#[fj_proc::attributed_arguments] +pub fn model( + #[value(default = 5, min = 3, max = 100)] num_points: u64, + #[value(default = 1.0, min = 1.0)] r1: f64, + #[value(min = 2.0)] r2: f64, + #[value(1.0)] h: f64, +) -> fj::Shape { + let num_vertices = num_points * 2; + let vertex_iter = (0..num_vertices).map(|i| { + let angle = Angle::from_rad(2. * PI / num_vertices as f64 * i as f64); + let radius = if i % 2 == 0 { r1 } else { r2 }; + (angle, radius) + }); + + // Now that we got that iterator prepared, generating the vertices is just a + // bit of trigonometry. + let mut outer = Vec::new(); + let mut inner = Vec::new(); + for (angle, radius) in vertex_iter { + let (sin, cos) = angle.rad().sin_cos(); + + let x = cos * radius; + let y = sin * radius; + + outer.push([x, y]); + inner.push([x / 2., y / 2.]); + } + + let outer = fj::Sketch::from_points(outer); + let inner = fj::Sketch::from_points(inner); + + let footprint = fj::Difference2d::from_shapes([outer.into(), inner.into()]); + + let star = fj::Sweep::from_path(footprint.into(), [0., 0., -h]); + + star.into() +} From efed2693f4330973070b4bf57f5b983e7c35e4e5 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Mon, 30 May 2022 09:27:34 +0200 Subject: [PATCH 7/8] Replace struct with function and add comments The struct `AttributeParameter`was only used for its `new` method, thus it was replaced by `determine_default()` Some comments were added to make the purpose of structs a bit clearer Signed-off-by: gabsi26 --- crates/fj-proc/src/attributed_arguments.rs | 95 +++++++++------------- 1 file changed, 38 insertions(+), 57 deletions(-) diff --git a/crates/fj-proc/src/attributed_arguments.rs b/crates/fj-proc/src/attributed_arguments.rs index e09e01de4..5bf480974 100644 --- a/crates/fj-proc/src/attributed_arguments.rs +++ b/crates/fj-proc/src/attributed_arguments.rs @@ -4,14 +4,13 @@ use syn::{ bracketed, parenthesized, parse::Parse, parse_macro_input, parse_quote, }; -pub fn attributed_arguments( - _default_values: TokenStream, - input: TokenStream, -) -> TokenStream { +pub fn attributed_arguments(_: TokenStream, input: TokenStream) -> TokenStream { let item = parse_macro_input!(input as syn::ItemFn); let inputs = item.clone().sig.inputs; + let args: Vec = inputs.iter().map(|inp| parse_quote!(#inp)).collect(); + let mut defaults = Vec::new(); let mut mins = Vec::new(); let mut maxs = Vec::new(); @@ -35,24 +34,13 @@ pub fn attributed_arguments( default = Some(value.val.clone()); } } - let attr_param = AttributeParameter::new(default, min, max); - defaults.push(attr_param.default); - mins.push(attr_param.min); - maxs.push(attr_param.max); + let [default, min, max] = determine_default(default, min, max); + defaults.push(default); + mins.push(min); + maxs.push(max); } let block = item.block; - println!( - "{}", - quote! { - #[no_mangle] - pub extern "C" fn model(args: &HashMap) -> fj::Shape { - #( - let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#defaults); - )* - #block - } - } - ); + quote! { #[no_mangle] pub extern "C" fn model(args: &HashMap) -> fj::Shape { @@ -83,6 +71,9 @@ impl Parse for Argument { } } +/// Represents all arguments given to the `#[value]` attribute eg: +/// `#[value(default=3, min=4)]` +/// ` ^^^^^^^^^^^^^^^^` #[derive(Debug, Clone)] struct HelperAttribute { pub values: syn::punctuated::Punctuated, @@ -115,6 +106,9 @@ impl Parse for HelperAttribute { } } +/// Represents one argument given to the `#[value]` attribute eg: +/// `#[value(default=3)]` +/// ` ^^^^^^^^^----- is parsed as DefaultParam{ ident: Some(default), val: 3 }` #[derive(Debug, Clone)] struct DefaultParam { pub ident: Option, @@ -139,46 +133,33 @@ impl Parse for DefaultParam { } } -#[derive(Debug)] -struct AttributeParameter { - pub default: Option, - pub min: Option, - pub max: Option, -} - -impl AttributeParameter { - // TODO: Checking the Options is quite ugly - pub fn new( - default: Option, - min: Option, - max: Option, - ) -> Self { - if let Some(default) = default { - let min = if min.is_some() { min } else { None }; - let max = if max.is_some() { max } else { None }; - Self { - default: Some(default), - min, - max, - } +/// Checks if a default value is supplied, otherwise applies either the min or max (if specified) as default. +fn determine_default( + default: Option, + min: Option, + max: Option, +) -> [Option; 3] { + if let Some(default) = default { + let min = if min.is_some() { min } else { None }; + let max = if max.is_some() { max } else { None }; + [Some(default), min, max] + } else { + let mut default = None; + let max = if max.is_some() { + default = max.clone(); + max } else { - let mut default = None; - let max = if max.is_some() { - default = max.clone(); - max - } else { - None - }; + None + }; - let min = if min.is_some() { - default = min.clone(); - min - } else { - None - }; + let min = if min.is_some() { + default = min.clone(); + min + } else { + None + }; - Self { default, min, max } - } + [default, min, max] } } From 78b47568246d00e0496c6e08a8b6f35c14411424 Mon Sep 17 00:00:00 2001 From: gabsi26 Date: Mon, 30 May 2022 09:36:04 +0200 Subject: [PATCH 8/8] Add one more comment and remove debug println! Signed-off-by: gabsi26 --- crates/fj-proc/src/attributed_arguments.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/fj-proc/src/attributed_arguments.rs b/crates/fj-proc/src/attributed_arguments.rs index 5bf480974..d2deccdf2 100644 --- a/crates/fj-proc/src/attributed_arguments.rs +++ b/crates/fj-proc/src/attributed_arguments.rs @@ -52,6 +52,11 @@ pub fn attributed_arguments(_: TokenStream, input: TokenStream) -> TokenStream { }.into() } +/// Represents one parameter given to the `model` +/// `#[value(default=3, min=4)] num_points: u64` +/// `^^^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~ ^^^-- ty` +/// ` | |` +/// ` attr ident` #[derive(Debug, Clone)] struct Argument { pub attr: HelperAttribute, @@ -65,8 +70,8 @@ impl Parse for Argument { let ident: proc_macro2::Ident = input.parse()?; let _: syn::token::Colon = input.parse()?; + let ty: proc_macro2::Ident = input.parse()?; - println!("attr: {:?}, ident: {:?}, ty: {:?}\n", attr, ident, ty); Ok(Self { attr, ident, ty }) } }