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