Skip to main content

ranim_macros/
lib.rs

1mod scene;
2mod utils;
3
4use proc_macro::TokenStream;
5use proc_macro_crate::{FoundCrate, crate_name};
6use proc_macro2::Span;
7use quote::quote;
8use syn::{Data, DeriveInput, Fields, Ident, ItemFn, parse_macro_input};
9
10use crate::scene::parse_scene_attrs;
11
12const RANIM_CRATE_NAME: &str = "ranim";
13
14fn ranim_path() -> proc_macro2::TokenStream {
15    match (
16        crate_name(RANIM_CRATE_NAME),
17        std::env::var("CARGO_CRATE_NAME").as_deref(),
18    ) {
19        (Ok(FoundCrate::Itself), Ok(RANIM_CRATE_NAME)) => quote!(crate),
20        (Ok(FoundCrate::Name(name)), _) => {
21            let ident = Ident::new(&name, Span::call_site());
22            quote!(::#ident)
23        }
24        _ => quote!(::ranim),
25    }
26}
27
28fn ranim_core_path() -> proc_macro2::TokenStream {
29    if let Ok(res) = crate_name("ranim-core") {
30        match (res, std::env::var("CARGO_CRATE_NAME").as_deref()) {
31            (FoundCrate::Itself, Ok("ranim-core") | Ok("ranim_core")) => return quote!(crate),
32            (FoundCrate::Name(name), _) => {
33                let ident = Ident::new(&name, Span::call_site());
34                return quote!(::#ident);
35            }
36            _ => (),
37        }
38    } else if let Ok(res) = crate_name("ranim") {
39        match (res, std::env::var("CARGO_CRATE_NAME").as_deref()) {
40            (FoundCrate::Itself, Ok("ranim")) => return quote!(crate::core),
41            (FoundCrate::Name(name), _) => {
42                let ident = Ident::new(&name, Span::call_site());
43                return quote!(::#ident::core);
44            }
45            _ => (),
46        }
47    }
48    ranim_path()
49}
50
51/// 解析单个属性(#[scene(...)] /  / #[output(...)])
52#[derive(Default)]
53struct SceneAttrs {
54    name: Option<String>,        // #[scene(name = "...")]
55    clear_color: Option<String>, // #[scene(clear_color = "#000000")]
56    wasm_demo_doc: bool,         // #[wasm_demo_doc]
57    outputs: Vec<OutputDef>,     // #[output(...)]
58}
59
60/// 一个 #[output(...)] 里的字段
61#[derive(Default)]
62struct OutputDef {
63    width: u32,
64    height: u32,
65    fps: u32,
66    save_frames: bool,
67    name: Option<String>,
68    name_template: Option<String>,
69    dir: String,
70    format: Option<String>,
71}
72
73// MARK: scene
74#[proc_macro_attribute]
75pub fn scene(args: TokenStream, input: TokenStream) -> TokenStream {
76    let ranim = ranim_path();
77    let input_fn = parse_macro_input!(input as ItemFn);
78    let attrs = parse_scene_attrs(args, input_fn.attrs.as_slice()).unwrap();
79
80    let fn_name = &input_fn.sig.ident;
81    let vis = &input_fn.vis;
82    let fn_body = &input_fn.block;
83    let doc_attrs: Vec<_> = input_fn
84        .attrs
85        .iter()
86        .filter(|attr| attr.path().is_ident("doc"))
87        .collect();
88
89    // 场景名称
90    let scene_name = attrs.name.unwrap_or_else(|| fn_name.to_string());
91
92    // StaticSceneConfig
93    let clear_color = attrs.clear_color.unwrap_or("#333333ff".to_string());
94    let scene_config = quote! {
95        #ranim::StaticSceneConfig {
96            clear_color: #clear_color,
97        }
98    };
99
100    // StaticOutput 列表
101    let mut outputs = Vec::new();
102    for OutputDef {
103        width,
104        height,
105        fps,
106        save_frames,
107        name,
108        name_template,
109        dir,
110        format,
111    } in attrs.outputs
112    {
113        let name_token = match name.as_deref() {
114            Some(n) if !n.is_empty() => quote! { Some(#n) },
115            _ => quote! { None },
116        };
117        let name_template_token = match name_template.as_deref() {
118            Some(n) if !n.is_empty() => quote! { Some(#n) },
119            _ => quote! { None },
120        };
121        let format_token = match format.as_deref() {
122            Some("mp4") | None => quote! { #ranim::OutputFormat::Mp4 },
123            Some("webm") => quote! { #ranim::OutputFormat::Webm },
124            Some("mov") => quote! { #ranim::OutputFormat::Mov },
125            Some("gif") => quote! { #ranim::OutputFormat::Gif },
126            Some(other) => panic!("unknown output format: {other:?}"),
127        };
128        outputs.push(quote! {
129            #ranim::StaticOutput {
130                width: #width,
131                height: #height,
132                fps: #fps,
133                save_frames: #save_frames,
134                name: #name_token,
135                name_template: #name_template_token,
136                dir: #dir,
137                format: #format_token,
138            }
139        });
140    }
141    if outputs.is_empty() {
142        outputs.push(quote! {
143            #ranim::StaticOutput::DEFAULT
144        });
145    }
146
147    let doc = if attrs.wasm_demo_doc {
148        quote! {
149            #[doc = concat!("<canvas id=\"ranim-app-", stringify!(#fn_name), "\" width=\"1280\" height=\"720\" style=\"width: 100%;\"></canvas>")]
150            #[doc = concat!("<script type=\"module\">")]
151            #[doc = concat!("  const { find_scene, preview_scene } = await ranim_examples;")]
152            #[doc = concat!("  preview_scene(find_scene(\"", stringify!(#fn_name), "\"));")]
153            #[doc = "</script>"]
154        }
155    } else {
156        quote! {}
157    };
158
159    let static_output_name = syn::Ident::new("__OUTPUTS", fn_name.span());
160    let static_scene_name = syn::Ident::new("__SCENE", fn_name.span());
161
162    let output_cnt = outputs.len();
163
164    let scene = quote! {
165        #ranim::StaticScene {
166            name: #scene_name,
167            constructor: super::#fn_name,
168            config: #scene_config,
169            outputs: &#static_output_name,
170        }
171    };
172
173    // ANCHOR: SCENE_MACRO
174    let expanded = quote! {
175        #doc
176        #(#doc_attrs)*
177        #vis fn #fn_name(r: &mut #ranim::RanimScene) #fn_body
178
179        #[doc(hidden)]
180        #vis mod #fn_name {
181            /// The static outputs.
182            pub static #static_output_name: [#ranim::StaticOutput; #output_cnt] = [#(#outputs),*];
183            /// The static scene descriptor.
184            pub static #static_scene_name: #ranim::StaticScene = #scene;
185            #ranim::inventory::submit!{
186                #scene
187            }
188
189            pub fn scene() -> #ranim::Scene {
190                #ranim::Scene::from(&#static_scene_name)
191            }
192        }
193    };
194    // ANCHOR_END: SCENE_MACRO
195
196    TokenStream::from(expanded)
197}
198
199/// Define a video output.
200///
201/// Default: 1920x1080 60fps, save_frames = false
202///
203/// Available attributes:
204/// - `width`: output width in pixels
205/// - `height`: output height in pixels
206/// - `fps`: frames per second
207/// - `save_frames`: save frames to disk
208/// - `dir`: directory for output
209/// - `name_template`: basename template for the output file
210#[proc_macro_attribute]
211pub fn output(_: TokenStream, _: TokenStream) -> TokenStream {
212    TokenStream::new()
213}
214
215// #[proc_macro_attribute]
216// pub fn preview(_: TokenStream, _: TokenStream) -> TokenStream {
217//     TokenStream::new()
218// }
219
220#[proc_macro_attribute]
221pub fn wasm_demo_doc(_attr: TokenStream, _: TokenStream) -> TokenStream {
222    TokenStream::new()
223}
224
225// MARK: derive Traits
226
227#[proc_macro_derive(Fill)]
228pub fn derive_fill(input: TokenStream) -> TokenStream {
229    let core = ranim_core_path();
230    impl_derive(input, quote! {#core::traits::Fill}, |field_positions| {
231        quote! {
232            fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
233                #(
234                    self.#field_positions.set_fill_opacity(opacity);
235                )*
236                self
237            }
238            fn fill_color(&self) -> #core::color::AlphaColor<#core::color::Srgb> {
239                [#(self.#field_positions.fill_color(), )*].first().cloned().unwrap()
240            }
241            fn set_fill_color(&mut self, color: #core::color::AlphaColor<#core::color::Srgb>) -> &mut Self {
242                #(
243                    self.#field_positions.set_fill_color(color);
244                )*
245                self
246            }
247        }
248    })
249}
250
251#[proc_macro_derive(Stroke)]
252pub fn derive_stroke(input: TokenStream) -> TokenStream {
253    let core = ranim_core_path();
254    impl_derive(input, quote! {#core::traits::Stroke}, |field_positions| {
255        quote! {
256            fn stroke_color(&self) -> #core::color::AlphaColor<#core::color::Srgb> {
257                [#(self.#field_positions.stroke_color(), )*].first().cloned().unwrap()
258            }
259            fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [#core::components::width::Width])) -> &mut Self {
260                #(
261                    self.#field_positions.apply_stroke_func(&f);
262                )*
263                self
264            }
265            fn set_stroke_color(&mut self, color: #core::color::AlphaColor<#core::color::Srgb>) -> &mut Self {
266                #(
267                    self.#field_positions.set_stroke_color(color);
268                )*
269                self
270            }
271            fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
272                #(
273                    self.#field_positions.set_stroke_opacity(opacity);
274                )*
275                self
276            }
277        }
278    })
279}
280
281#[proc_macro_derive(Partial)]
282pub fn derive_partial(input: TokenStream) -> TokenStream {
283    let core = ranim_core_path();
284    impl_derive(input, quote! {#core::traits::Partial}, |field_positions| {
285        quote! {
286            fn get_partial(&self, range: std::ops::Range<f64>) -> Self {
287                Self {
288                    #(
289                        #field_positions: self.#field_positions.get_partial(range.clone()),
290                    )*
291                }
292            }
293            fn get_partial_closed(&self, range: std::ops::Range<f64>) -> Self {
294                Self {
295                    #(
296                        #field_positions: self.#field_positions.get_partial(range.clone()),
297                    )*
298                }
299            }
300        }
301    })
302}
303
304#[proc_macro_derive(Empty)]
305pub fn derive_empty(input: TokenStream) -> TokenStream {
306    let core = ranim_core_path();
307    let input = parse_macro_input!(input as DeriveInput);
308    let name = &input.ident;
309    let generics = &input.generics;
310    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
311
312    let fields = match &input.data {
313        Data::Struct(data) => &data.fields,
314        _ => panic!("Empty can only be derived for structs"),
315    };
316
317    let field_impls = match fields {
318        Fields::Named(fields) => {
319            let (field_names, field_types): (Vec<_>, Vec<_>) =
320                fields.named.iter().map(|f| (&f.ident, &f.ty)).unzip();
321
322            quote! {
323                Self {
324                    #(
325                        #field_names: #field_types::empty(),
326                    )*
327                }
328            }
329        }
330        Fields::Unnamed(fields) => {
331            let field_types = fields.unnamed.iter().map(|f| &f.ty);
332            quote! {
333                Self (
334                    #(
335                        #field_types::empty(),
336                    )*
337                )
338            }
339        }
340        Fields::Unit => quote! {},
341    };
342
343    let expanded = quote! {
344        impl #impl_generics #core::traits::Empty for #name #ty_generics #where_clause {
345            fn empty() -> Self {
346                #field_impls
347            }
348        }
349    };
350
351    TokenStream::from(expanded)
352}
353
354#[proc_macro_derive(Opacity)]
355pub fn derive_opacity(input: TokenStream) -> TokenStream {
356    let core = ranim_core_path();
357    impl_derive(input, quote! {#core::traits::Opacity}, |field_positions| {
358        quote! {
359            fn set_opacity(&mut self, opacity: f32) -> &mut Self {
360                #(
361                    self.#field_positions.set_opacity(opacity);
362                )*
363                self
364            }
365        }
366    })
367}
368
369#[proc_macro_derive(Alignable)]
370pub fn derive_alignable(input: TokenStream) -> TokenStream {
371    let core = ranim_core_path();
372    impl_derive(
373        input,
374        quote! {#core::traits::Alignable},
375        |field_positions| {
376            quote! {
377                fn is_aligned(&self, other: &Self) -> bool {
378                    #(
379                        self.#field_positions.is_aligned(&other.#field_positions) &&
380                    )* true
381                }
382                fn align_with(&mut self, other: &mut Self) {
383                    #(
384                        self.#field_positions.align_with(&mut other.#field_positions);
385                    )*
386                }
387            }
388        },
389    )
390}
391
392#[proc_macro_derive(Interpolatable)]
393pub fn derive_interpolatable(input: TokenStream) -> TokenStream {
394    let core = ranim_core_path();
395    impl_derive(
396        input,
397        quote! {#core::traits::Interpolatable},
398        |field_positions| {
399            quote! {
400                fn lerp(&self, other: &Self, t: f64) -> Self {
401                    Self {
402                        #(
403                            #field_positions: #core::traits::Interpolatable::lerp(&self.#field_positions, &other.#field_positions, t),
404                        )*
405                    }
406                }
407            }
408        },
409    )
410}
411
412#[proc_macro_derive(ShiftTransform)]
413pub fn derive_shift_impl(input: TokenStream) -> TokenStream {
414    let core = ranim_core_path();
415    impl_derive(
416        input,
417        quote! {#core::traits::ShiftTransform},
418        |field_positions| {
419            quote! {
420                fn shift(&mut self, shift: #core::glam::DVec3) -> &mut Self {
421                    #(self.#field_positions.shift(shift);)*
422                    self
423                }
424            }
425        },
426    )
427}
428
429#[proc_macro_derive(RotateTransform)]
430pub fn derive_rotate_impl(input: TokenStream) -> TokenStream {
431    let core = ranim_core_path();
432    impl_derive(
433        input,
434        quote! {#core::traits::RotateTransform},
435        |field_positions| {
436            quote! {
437                fn rotate_on_axis(&mut self, axis: #core::glam::DVec3, angle: f64) -> &mut Self {
438                    #(self.#field_positions.rotate_on_axis(axis, angle);)*
439                    self
440                }
441            }
442        },
443    )
444}
445
446#[proc_macro_derive(ScaleTransform)]
447pub fn derive_scale_impl(input: TokenStream) -> TokenStream {
448    let core = ranim_core_path();
449    impl_derive(
450        input,
451        quote! {#core::traits::ScaleTransform},
452        |field_positions| {
453            quote! {
454                fn scale(&mut self, scale: #core::glam::DVec3) -> &mut Self {
455                    #(self.#field_positions.scale(scale);)*
456                    self
457                }
458            }
459        },
460    )
461}
462
463#[proc_macro_derive(PointsFunc)]
464pub fn derive_point_func(input: TokenStream) -> TokenStream {
465    let core = ranim_core_path();
466    impl_derive(
467        input,
468        quote! {#core::traits::PointsFunc},
469        |field_positions| {
470            quote! {
471                fn apply_points_func(&mut self, f: impl for<'a> Fn(&'a mut [DVec3])) -> &mut Self {
472                    #(self.#field_positions.apply_points_func(f);)*
473                    self
474                }
475            }
476        },
477    )
478}
479
480fn impl_derive(
481    input: TokenStream,
482    trait_path: proc_macro2::TokenStream,
483    impl_token: impl Fn(Vec<proc_macro2::TokenStream>) -> proc_macro2::TokenStream,
484) -> TokenStream {
485    let input = parse_macro_input!(input as DeriveInput);
486    let name = &input.ident;
487    let generics = &input.generics;
488    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
489
490    let fields = match &input.data {
491        Data::Struct(data) => &data.fields,
492        _ => panic!("Can only be derived for structs"),
493    };
494
495    let field_positions = get_field_positions(fields)
496        .ok_or("cannot get field from unit struct")
497        .unwrap();
498
499    let impl_token = impl_token(field_positions);
500    let expanded = quote! {
501        impl #impl_generics #trait_path for #name #ty_generics #where_clause {
502            #impl_token
503        }
504    };
505
506    TokenStream::from(expanded)
507}
508
509fn get_field_positions(fields: &Fields) -> Option<Vec<proc_macro2::TokenStream>> {
510    match fields {
511        Fields::Named(fields) => Some(
512            fields
513                .named
514                .iter()
515                .map(|f| {
516                    let pos = &f.ident;
517                    quote! { #pos }
518                })
519                .collect::<Vec<_>>(),
520        ),
521        Fields::Unnamed(fields) => Some(
522            (0..fields.unnamed.len())
523                .map(syn::Index::from)
524                .map(|i| {
525                    quote! { #i }
526                })
527                .collect::<Vec<_>>(),
528        ),
529        Fields::Unit => None,
530    }
531}