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    let expanded = quote! {
167        #doc
168        #(#doc_attrs)*
169        #vis fn #fn_name(r: &mut #ranim::RanimScene) #fn_body
170
171        #[doc(hidden)]
172        #vis mod #fn_name {
173            /// The static outputs.
174            pub static #static_output_name: [#ranim::StaticOutput; #output_cnt] = [#(#outputs),*];
175            /// The static scene descriptor.
176            pub static #static_scene_name: #ranim::StaticScene = #scene;
177            #ranim::inventory::submit!{
178                #scene
179            }
180
181            pub fn scene() -> #ranim::Scene {
182                #ranim::Scene::from(&#static_scene_name)
183            }
184        }
185    };
186
187    TokenStream::from(expanded)
188}
189
190/// Define a video output.
191///
192/// Default: 1920x1080 60fps, save_frames = false
193///
194/// Available attributes:
195/// - `pixel_size`: (width, height)
196/// - `fps`: frames per second
197/// - `save_frames`: save frames to disk
198/// - `dir`: directory for output
199#[proc_macro_attribute]
200pub fn output(_: TokenStream, _: TokenStream) -> TokenStream {
201    TokenStream::new()
202}
203
204// #[proc_macro_attribute]
205// pub fn preview(_: TokenStream, _: TokenStream) -> TokenStream {
206//     TokenStream::new()
207// }
208
209#[proc_macro_attribute]
210pub fn wasm_demo_doc(_attr: TokenStream, _: TokenStream) -> TokenStream {
211    TokenStream::new()
212}
213
214// MARK: derive Traits
215
216#[proc_macro_derive(Fill)]
217pub fn derive_fill(input: TokenStream) -> TokenStream {
218    let core = ranim_core_path();
219    impl_derive(input, quote! {#core::traits::Fill}, |field_positions| {
220        quote! {
221            fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
222                #(
223                    self.#field_positions.set_fill_opacity(opacity);
224                )*
225                self
226            }
227            fn fill_color(&self) -> #core::color::AlphaColor<#core::color::Srgb> {
228                [#(self.#field_positions.fill_color(), )*].first().cloned().unwrap()
229            }
230            fn set_fill_color(&mut self, color: #core::color::AlphaColor<#core::color::Srgb>) -> &mut Self {
231                #(
232                    self.#field_positions.set_fill_color(color);
233                )*
234                self
235            }
236        }
237    })
238}
239
240#[proc_macro_derive(Stroke)]
241pub fn derive_stroke(input: TokenStream) -> TokenStream {
242    let core = ranim_core_path();
243    impl_derive(input, quote! {#core::traits::Stroke}, |field_positions| {
244        quote! {
245            fn stroke_color(&self) -> #core::color::AlphaColor<#core::color::Srgb> {
246                [#(self.#field_positions.stroke_color(), )*].first().cloned().unwrap()
247            }
248            fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [#core::components::width::Width])) -> &mut Self {
249                #(
250                    self.#field_positions.apply_stroke_func(&f);
251                )*
252                self
253            }
254            fn set_stroke_color(&mut self, color: #core::color::AlphaColor<#core::color::Srgb>) -> &mut Self {
255                #(
256                    self.#field_positions.set_stroke_color(color);
257                )*
258                self
259            }
260            fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
261                #(
262                    self.#field_positions.set_stroke_opacity(opacity);
263                )*
264                self
265            }
266        }
267    })
268}
269
270#[proc_macro_derive(Partial)]
271pub fn derive_partial(input: TokenStream) -> TokenStream {
272    let core = ranim_core_path();
273    impl_derive(input, quote! {#core::traits::Partial}, |field_positions| {
274        quote! {
275            fn get_partial(&self, range: std::ops::Range<f64>) -> Self {
276                Self {
277                    #(
278                        #field_positions: self.#field_positions.get_partial(range.clone()),
279                    )*
280                }
281            }
282            fn get_partial_closed(&self, range: std::ops::Range<f64>) -> Self {
283                Self {
284                    #(
285                        #field_positions: self.#field_positions.get_partial(range.clone()),
286                    )*
287                }
288            }
289        }
290    })
291}
292
293#[proc_macro_derive(Empty)]
294pub fn derive_empty(input: TokenStream) -> TokenStream {
295    let core = ranim_core_path();
296    let input = parse_macro_input!(input as DeriveInput);
297    let name = &input.ident;
298    let generics = &input.generics;
299    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
300
301    let fields = match &input.data {
302        Data::Struct(data) => &data.fields,
303        _ => panic!("Empty can only be derived for structs"),
304    };
305
306    let field_impls = match fields {
307        Fields::Named(fields) => {
308            let (field_names, field_types): (Vec<_>, Vec<_>) =
309                fields.named.iter().map(|f| (&f.ident, &f.ty)).unzip();
310
311            quote! {
312                Self {
313                    #(
314                        #field_names: #field_types::empty(),
315                    )*
316                }
317            }
318        }
319        Fields::Unnamed(fields) => {
320            let field_types = fields.unnamed.iter().map(|f| &f.ty);
321            quote! {
322                Self (
323                    #(
324                        #field_types::empty(),
325                    )*
326                )
327            }
328        }
329        Fields::Unit => quote! {},
330    };
331
332    let expanded = quote! {
333        impl #impl_generics #core::traits::Empty for #name #ty_generics #where_clause {
334            fn empty() -> Self {
335                #field_impls
336            }
337        }
338    };
339
340    TokenStream::from(expanded)
341}
342
343#[proc_macro_derive(Opacity)]
344pub fn derive_opacity(input: TokenStream) -> TokenStream {
345    let core = ranim_core_path();
346    impl_derive(input, quote! {#core::traits::Opacity}, |field_positions| {
347        quote! {
348            fn set_opacity(&mut self, opacity: f32) -> &mut Self {
349                #(
350                    self.#field_positions.set_opacity(opacity);
351                )*
352                self
353            }
354        }
355    })
356}
357
358#[proc_macro_derive(Alignable)]
359pub fn derive_alignable(input: TokenStream) -> TokenStream {
360    let core = ranim_core_path();
361    impl_derive(
362        input,
363        quote! {#core::traits::Alignable},
364        |field_positions| {
365            quote! {
366                fn is_aligned(&self, other: &Self) -> bool {
367                    #(
368                        self.#field_positions.is_aligned(&other.#field_positions) &&
369                    )* true
370                }
371                fn align_with(&mut self, other: &mut Self) {
372                    #(
373                        self.#field_positions.align_with(&mut other.#field_positions);
374                    )*
375                }
376            }
377        },
378    )
379}
380
381#[proc_macro_derive(Interpolatable)]
382pub fn derive_interpolatable(input: TokenStream) -> TokenStream {
383    let core = ranim_core_path();
384    impl_derive(
385        input,
386        quote! {#core::traits::Interpolatable},
387        |field_positions| {
388            quote! {
389                fn lerp(&self, other: &Self, t: f64) -> Self {
390                    Self {
391                        #(
392                            #field_positions: #core::traits::Interpolatable::lerp(&self.#field_positions, &other.#field_positions, t),
393                        )*
394                    }
395                }
396            }
397        },
398    )
399}
400
401#[proc_macro_derive(ShiftTransform)]
402pub fn derive_shift_impl(input: TokenStream) -> TokenStream {
403    let core = ranim_core_path();
404    impl_derive(
405        input,
406        quote! {#core::traits::ShiftTransform},
407        |field_positions| {
408            quote! {
409                fn shift(&mut self, shift: #core::glam::DVec3) -> &mut Self {
410                    #(self.#field_positions.shift(shift);)*
411                    self
412                }
413            }
414        },
415    )
416}
417
418#[proc_macro_derive(RotateTransform)]
419pub fn derive_rotate_impl(input: TokenStream) -> TokenStream {
420    let core = ranim_core_path();
421    impl_derive(
422        input,
423        quote! {#core::traits::RotateTransform},
424        |field_positions| {
425            quote! {
426                fn rotate_on_axis(&mut self, axis: #core::glam::DVec3, angle: f64) -> &mut Self {
427                    #(self.#field_positions.rotate_on_axis(axis, angle);)*
428                    self
429                }
430            }
431        },
432    )
433}
434
435#[proc_macro_derive(ScaleTransform)]
436pub fn derive_scale_impl(input: TokenStream) -> TokenStream {
437    let core = ranim_core_path();
438    impl_derive(
439        input,
440        quote! {#core::traits::ScaleTransform},
441        |field_positions| {
442            quote! {
443                fn scale(&mut self, scale: #core::glam::DVec3) -> &mut Self {
444                    #(self.#field_positions.scale(scale);)*
445                    self
446                }
447            }
448        },
449    )
450}
451
452#[proc_macro_derive(PointsFunc)]
453pub fn derive_point_func(input: TokenStream) -> TokenStream {
454    let core = ranim_core_path();
455    impl_derive(
456        input,
457        quote! {#core::traits::PointsFunc},
458        |field_positions| {
459            quote! {
460                fn apply_points_func(&mut self, f: impl for<'a> Fn(&'a mut [DVec3])) -> &mut Self {
461                    #(self.#field_positions.apply_points_func(f);)*
462                    self
463                }
464            }
465        },
466    )
467}
468
469fn impl_derive(
470    input: TokenStream,
471    trait_path: proc_macro2::TokenStream,
472    impl_token: impl Fn(Vec<proc_macro2::TokenStream>) -> proc_macro2::TokenStream,
473) -> TokenStream {
474    let input = parse_macro_input!(input as DeriveInput);
475    let name = &input.ident;
476    let generics = &input.generics;
477    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
478
479    let fields = match &input.data {
480        Data::Struct(data) => &data.fields,
481        _ => panic!("Can only be derived for structs"),
482    };
483
484    let field_positions = get_field_positions(fields)
485        .ok_or("cannot get field from unit struct")
486        .unwrap();
487
488    let impl_token = impl_token(field_positions);
489    let expanded = quote! {
490        impl #impl_generics #trait_path for #name #ty_generics #where_clause {
491            #impl_token
492        }
493    };
494
495    TokenStream::from(expanded)
496}
497
498fn get_field_positions(fields: &Fields) -> Option<Vec<proc_macro2::TokenStream>> {
499    match fields {
500        Fields::Named(fields) => Some(
501            fields
502                .named
503                .iter()
504                .map(|f| {
505                    let pos = &f.ident;
506                    quote! { #pos }
507                })
508                .collect::<Vec<_>>(),
509        ),
510        Fields::Unnamed(fields) => Some(
511            (0..fields.unnamed.len())
512                .map(syn::Index::from)
513                .map(|i| {
514                    quote! { #i }
515                })
516                .collect::<Vec<_>>(),
517        ),
518        Fields::Unit => None,
519    }
520}