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