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