Skip to content

Commit

Permalink
Merge pull request #643 from gabsi26/proc_me_up_scotty
Browse files Browse the repository at this point in the history
Provides two macros to make model creation more convenient
  • Loading branch information
hannobraun authored May 31, 2022
2 parents dea6026 + 78b4756 commit 3daab76
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 33 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ members = [
"crates/fj-math",
"crates/fj-operations",
"crates/fj-viewer",
"crates/fj-proc",

"crates/fj-window",

"crates/fj-proc",


"models/cuboid",
"models/spacer",
"models/star",
"models/star_attributed_arguments",
"models/test",

"tools/export-validator",
Expand Down
22 changes: 22 additions & 0 deletions crates/fj-proc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
198 changes: 198 additions & 0 deletions crates/fj-proc/src/attributed_arguments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
bracketed, parenthesized, parse::Parse, parse_macro_input, parse_quote,
};

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<Argument> =
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 [default, min, max] = determine_default(default, min, max);
defaults.push(default);
mins.push(min);
maxs.push(max);
}
let block = item.block;

quote! {
#[no_mangle]
pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape {
#(
let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#defaults);
)*
#block
}
}.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,
pub ident: proc_macro2::Ident,
pub ty: proc_macro2::Ident,
}

impl Parse for Argument {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
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()?;
Ok(Self { attr, ident, ty })
}
}

/// Represents all arguments given to the `#[value]` attribute eg:
/// `#[value(default=3, min=4)]`
/// ` ^^^^^^^^^^^^^^^^`
#[derive(Debug, Clone)]
struct HelperAttribute {
pub values: syn::punctuated::Punctuated<DefaultParam, syn::Token![,]>,
}

impl Parse for HelperAttribute {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
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,
)?,
})
}
}

/// 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<proc_macro2::Ident>,
pub val: syn::Expr,
}

impl Parse for DefaultParam {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(syn::Ident) {
let ident: Option<proc_macro2::Ident> = Some(input.parse()?);
let _: syn::token::Eq = input.parse()?;
Ok(Self {
ident,
val: input.parse()?,
})
} else {
Ok(Self {
ident: None,
val: input.parse()?,
})
}
}
}

/// Checks if a default value is supplied, otherwise applies either the min or max (if specified) as default.
fn determine_default(
default: Option<syn::Expr>,
min: Option<syn::Expr>,
max: Option<syn::Expr>,
) -> [Option<syn::Expr>; 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 {
None
};

let min = if min.is_some() {
default = min.clone();
min
} else {
None
};

[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<String, String>) -> 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);

// }
76 changes: 76 additions & 0 deletions crates/fj-proc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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<String> = 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<String, String>) -> fj::Shape {
#(let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#vals.parse::<#types>().unwrap());)*
#block
}
}.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 {
// }

// #[no_mangle]
// pub extern "C" fn model(args: &HashMap<String, String>) -> 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);

// }
3 changes: 3 additions & 0 deletions models/star/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ crate-type = ["cdylib"]

[dependencies.fj]
path = "../../crates/fj"

[dependencies.fj-proc]
path = "../../crates/fj-proc"
37 changes: 4 additions & 33 deletions models/star/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>) -> 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);
Expand All @@ -59,7 +30,7 @@ pub extern "C" fn model(args: &HashMap<String, String>) -> 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()
}
13 changes: 13 additions & 0 deletions models/star_attributed_arguments/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading

0 comments on commit 3daab76

Please sign in to comment.