rdpe_derive/
lib.rs

1//! Derive macros for the RDPE particle simulation engine.
2#![allow(clippy::type_complexity)]
3//!
4//! This crate provides three derive macros:
5//!
6//! - [`Particle`] - Generates GPU-compatible structs and WGSL code
7//! - [`ParticleType`] - Creates type-safe enums for particle categories
8//! - [`MultiParticle`] - Combines multiple Particle types into one simulation
9//!
10//! # Usage
11//!
12//! These macros are re-exported from the main `rdpe` crate. You don't need
13//! to add this crate directly:
14//!
15//! ```ignore
16//! use rdpe::prelude::*;
17//!
18//! #[derive(Particle, Clone)]
19//! struct Ball {
20//!     position: Vec3,
21//!     velocity: Vec3,
22//! }
23//!
24//! #[derive(ParticleType, Clone, Copy, PartialEq)]
25//! enum Species {
26//!     Prey,
27//!     Predator,
28//! }
29//! ```
30//!
31//! # The Particle Macro
32//!
33//! `#[derive(Particle)]` transforms your Rust struct into a GPU-compatible
34//! format. It generates:
35//!
36//! - A companion `{Name}Gpu` struct with proper alignment and padding
37//! - A `WGSL_STRUCT` constant containing the WGSL struct definition
38//! - `to_gpu()` method for converting Rust → GPU format
39//!
40//! ## Required Fields
41//!
42//! Every particle must have:
43//! - `position: Vec3` - Particle position in 3D space
44//! - `velocity: Vec3` - Particle velocity
45//!
46//! ## Optional Fields
47//!
48//! - `particle_type: u32` - For typed interactions (auto-added if missing)
49//! - `#[color] color: Vec3` - Custom particle color
50//! - Any `f32`, `u32`, `i32`, `Vec2`, `Vec3`, `Vec4` fields
51//!
52//! ## GPU Memory Layout
53//!
54//! The macro handles WGSL's strict alignment requirements:
55//! - `Vec3` requires 16-byte alignment (even though it's only 12 bytes)
56//! - Struct total size must be a multiple of 16 bytes
57//! - Padding fields are automatically inserted
58//!
59//! # The ParticleType Macro
60//!
61//! `#[derive(ParticleType)]` enables type-safe particle categories for use
62//! with typed rules like `Chase`, `Evade`, and `Convert`.
63//!
64//! It generates:
65//! - `From<EnumName> for u32` - Convert enum to GPU-compatible integer
66//! - `From<u32> for EnumName` - Convert back (defaults to first variant)
67//! - `EnumName::count() -> u32` - Number of variants
68
69use proc_macro::TokenStream;
70use proc_macro2::Span;
71use quote::quote;
72use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type};
73
74/// Derive macro for particle type enums.
75///
76/// Creates type-safe particle categories that convert to/from `u32` for GPU usage.
77/// Variants are assigned sequential IDs starting from 0.
78///
79/// # Generated Items
80///
81/// For an enum `Species`:
82///
83/// - `impl From<Species> for u32` - Convert variant to integer
84/// - `impl From<u32> for Species` - Convert integer to variant (invalid values default to first variant)
85/// - `Species::count() -> u32` - Returns number of variants
86///
87/// # Requirements
88///
89/// - Must be an enum (not a struct)
90/// - All variants must be unit variants (no fields)
91/// - Enum should also derive `Clone`, `Copy`, `PartialEq` for typical usage
92///
93/// # Example
94///
95/// ```ignore
96/// #[derive(ParticleType, Clone, Copy, PartialEq)]
97/// enum Species {
98///     Prey,      // = 0
99///     Predator,  // = 1
100///     Plant,     // = 2
101/// }
102///
103/// // Convert to u32 for rules
104/// let prey_id: u32 = Species::Prey.into();  // 0
105///
106/// // Use with typed rules
107/// Rule::Chase {
108///     self_type: Species::Predator.into(),
109///     target_type: Species::Prey.into(),
110///     radius: 0.3,
111///     strength: 2.0,
112/// }
113///
114/// // Use in spawner
115/// Creature {
116///     position: pos,
117///     velocity: Vec3::ZERO,
118///     particle_type: Species::Prey.into(),
119/// }
120///
121/// // Get variant count
122/// let num_species = Species::count();  // 3
123/// ```
124///
125/// # Panics
126///
127/// The macro panics at compile time if:
128/// - Applied to a struct instead of an enum
129/// - Any variant has fields (tuple or struct variants)
130/// - Enum has zero variants
131#[proc_macro_derive(ParticleType)]
132pub fn derive_particle_type(input: TokenStream) -> TokenStream {
133    let input = parse_macro_input!(input as DeriveInput);
134    let name = &input.ident;
135
136    let variants = match &input.data {
137        Data::Enum(data) => &data.variants,
138        _ => panic!("ParticleType derive only supports enums"),
139    };
140
141    // Check that all variants are unit variants (no fields)
142    for variant in variants.iter() {
143        if !matches!(variant.fields, Fields::Unit) {
144            panic!(
145                "ParticleType enum variants must be unit variants (no fields). \
146                 Found fields on variant '{}'",
147                variant.ident
148            );
149        }
150    }
151
152    // Generate match arms for Into<u32>
153    let into_arms: Vec<_> = variants
154        .iter()
155        .enumerate()
156        .map(|(i, variant)| {
157            let variant_name = &variant.ident;
158            let idx = i as u32;
159            quote! { #name::#variant_name => #idx }
160        })
161        .collect();
162
163    // Generate match arms for From<u32>
164    let from_arms: Vec<_> = variants
165        .iter()
166        .enumerate()
167        .map(|(i, variant)| {
168            let variant_name = &variant.ident;
169            let idx = i as u32;
170            quote! { #idx => #name::#variant_name }
171        })
172        .collect();
173
174    let first_variant = &variants.first().expect("Enum must have at least one variant").ident;
175    let variant_count = variants.len() as u32;
176
177    let expanded = quote! {
178        impl From<#name> for u32 {
179            fn from(value: #name) -> u32 {
180                match value {
181                    #(#into_arms),*
182                }
183            }
184        }
185
186        impl From<u32> for #name {
187            fn from(value: u32) -> #name {
188                match value {
189                    #(#from_arms,)*
190                    _ => #name::#first_variant, // Default to first variant for invalid values
191                }
192            }
193        }
194
195        impl #name {
196            /// Returns the number of variants in this particle type enum.
197            pub const fn count() -> u32 {
198                #variant_count
199            }
200        }
201    };
202
203    TokenStream::from(expanded)
204}
205
206/// Derive macro for particle structs.
207///
208/// Transforms a Rust struct into a GPU-compatible particle type. Generates:
209///
210/// - A companion `{Name}Gpu` struct with `#[repr(C)]` and proper WGSL alignment
211/// - Implementation of [`ParticleTrait`](rdpe::ParticleTrait)
212/// - WGSL struct definition as a const string
213///
214/// # Required Fields
215///
216/// Every particle must have these fields:
217///
218/// | Field | Type | Purpose |
219/// |-------|------|---------|
220/// | `position` | `Vec3` | Particle location in 3D space |
221/// | `velocity` | `Vec3` | Particle movement direction and speed |
222///
223/// # Optional Fields
224///
225/// | Field | Type | Purpose |
226/// |-------|------|---------|
227/// | `particle_type` | `u32` | Category for typed interactions (auto-added if missing) |
228/// | `#[color] name` | `Vec3` | Custom particle color (RGB, 0.0-1.0) |
229/// | *(any name)* | `f32`, `u32`, `i32`, `Vec2`, `Vec3`, `Vec4` | Custom data |
230///
231/// # Supported Types
232///
233/// | Rust Type | WGSL Type | Size | Alignment |
234/// |-----------|-----------|------|-----------|
235/// | `Vec3` | `vec3<f32>` | 12 bytes | 16 bytes |
236/// | `Vec2` | `vec2<f32>` | 8 bytes | 8 bytes |
237/// | `Vec4` | `vec4<f32>` | 16 bytes | 16 bytes |
238/// | `f32` | `f32` | 4 bytes | 4 bytes |
239/// | `u32` | `u32` | 4 bytes | 4 bytes |
240/// | `i32` | `i32` | 4 bytes | 4 bytes |
241///
242/// # GPU Memory Layout
243///
244/// WGSL has strict alignment requirements that differ from Rust. This macro
245/// automatically inserts padding fields to ensure correct GPU memory layout:
246///
247/// - `Vec3` requires 16-byte alignment (despite being 12 bytes)
248/// - Arrays of structs require 16-byte stride
249/// - Padding fields are named `_pad0`, `_pad1`, etc.
250///
251/// # The `#[color]` Attribute
252///
253/// Mark a `Vec3` field with `#[color]` to use it for particle rendering:
254///
255/// ```ignore
256/// #[derive(Particle, Clone)]
257/// struct Firework {
258///     position: Vec3,
259///     velocity: Vec3,
260///     #[color]
261///     color: Vec3,  // RGB values 0.0-1.0
262/// }
263/// ```
264///
265/// Without `#[color]`, particles are colored based on their position.
266///
267/// # Example
268///
269/// ```ignore
270/// use rdpe::prelude::*;
271///
272/// // Minimal particle
273/// #[derive(Particle, Clone)]
274/// struct Basic {
275///     position: Vec3,
276///     velocity: Vec3,
277/// }
278///
279/// // Full-featured particle
280/// #[derive(Particle, Clone)]
281/// struct Advanced {
282///     position: Vec3,
283///     velocity: Vec3,
284///     #[color]
285///     color: Vec3,
286///     particle_type: u32,
287///     energy: f32,
288///     age: f32,
289/// }
290/// ```
291///
292/// # Generated Code
293///
294/// For a particle `Ball`, the macro generates:
295///
296/// ```ignore
297/// // GPU-compatible struct
298/// #[repr(C)]
299/// #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
300/// pub struct BallGpu {
301///     pub position: [f32; 3],
302///     pub _pad0: f32,  // Alignment padding
303///     pub velocity: [f32; 3],
304///     pub _pad1: f32,
305///     pub particle_type: u32,
306///     pub _pad2: [f32; 3],  // Struct size padding
307/// }
308///
309/// impl ParticleTrait for Ball {
310///     type Gpu = BallGpu;
311///     const WGSL_STRUCT: &'static str = "...";
312///     // ...
313/// }
314/// ```
315///
316/// # Panics
317///
318/// The macro panics at compile time if:
319/// - Applied to an enum instead of a struct
320/// - Struct uses tuple fields instead of named fields
321/// - Any field has an unsupported type
322#[proc_macro_derive(Particle, attributes(color))]
323pub fn derive_particle(input: TokenStream) -> TokenStream {
324    let input = parse_macro_input!(input as DeriveInput);
325    let name = &input.ident;
326    let gpu_name = Ident::new(&format!("{}Gpu", name), Span::call_site());
327
328    let fields = match &input.data {
329        Data::Struct(data) => match &data.fields {
330            Fields::Named(fields) => &fields.named,
331            _ => panic!("Particle derive only supports structs with named fields"),
332        },
333        _ => panic!("Particle derive only supports structs"),
334    };
335
336    let mut wgsl_fields = Vec::new();
337    let mut gpu_struct_fields = Vec::new();
338    let mut to_gpu_conversions = Vec::new();
339    let mut from_gpu_conversions = Vec::new();
340    let mut inspect_field_entries = Vec::new();
341    let mut editable_field_widgets = Vec::new();
342    let mut field_offset = 0u32;
343    let mut padding_count = 0u32;
344    let mut color_field: Option<String> = None;
345    let mut color_offset: Option<u32> = None;
346    let mut has_particle_type = false;
347
348    for field in fields.iter() {
349        let field_name = field.ident.as_ref().unwrap();
350        if field_name == "particle_type" {
351            has_particle_type = true;
352        }
353    }
354
355    for field in fields.iter() {
356        let field_name = field.ident.as_ref().unwrap();
357        let field_name_str = field_name.to_string();
358        let field_type = &field.ty;
359        let type_info = rust_type_info(field_type);
360
361        // Check for #[color] attribute - need to track offset before padding
362        let mut is_color_field = false;
363        for attr in &field.attrs {
364            if attr.path().is_ident("color") {
365                color_field = Some(field_name_str.clone());
366                is_color_field = true;
367            }
368        }
369
370        // Add padding before field if needed for alignment
371        let padding_needed = (type_info.align - (field_offset % type_info.align)) % type_info.align;
372        if padding_needed > 0 {
373            let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
374            let pad_name_str = format!("_pad{}", padding_count);
375            padding_count += 1;
376
377            if padding_needed == 4 {
378                wgsl_fields.push(format!("    {}: f32,", pad_name_str));
379                gpu_struct_fields.push(quote! { #pad_name: f32 });
380                to_gpu_conversions.push(quote! { #pad_name: 0.0 });
381            } else {
382                let count = (padding_needed / 4) as usize;
383                wgsl_fields.push(format!("    {}: array<f32, {}>,", pad_name_str, count));
384                gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
385                to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
386            }
387            field_offset += padding_needed;
388        }
389
390        // Record color field offset (after padding, before adding size)
391        if is_color_field {
392            color_offset = Some(field_offset);
393        }
394
395        // Add the actual field
396        wgsl_fields.push(format!("    {}: {},", field_name_str, type_info.wgsl_type));
397
398        let gpu_field_type = type_info.gpu_type;
399        gpu_struct_fields.push(quote! { #field_name: #gpu_field_type });
400
401        let conversion = generate_conversion(field_name, field_type);
402        to_gpu_conversions.push(quote! { #field_name: #conversion });
403
404        let reverse_conversion = generate_reverse_conversion(field_name, field_type);
405        from_gpu_conversions.push(quote! { #field_name: #reverse_conversion });
406
407        // Generate inspect field entry with nice formatting
408        let inspect_format = generate_inspect_format(field_name, field_type);
409        inspect_field_entries.push(quote! { (#field_name_str, #inspect_format) });
410
411        // Generate editable widget for this field
412        let editable_widget = generate_editable_widget(field_name, &field_name_str, field_type);
413        editable_field_widgets.push(editable_widget);
414
415        field_offset += type_info.size;
416    }
417
418    // Add particle_type field if user didn't provide one
419    if !has_particle_type {
420        // u32 has 4-byte alignment
421        let padding_needed = (4 - (field_offset % 4)) % 4;
422        if padding_needed > 0 {
423            let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
424            let pad_name_str = format!("_pad{}", padding_count);
425            padding_count += 1;
426
427            if padding_needed == 4 {
428                wgsl_fields.push(format!("    {}: f32,", pad_name_str));
429                gpu_struct_fields.push(quote! { #pad_name: f32 });
430                to_gpu_conversions.push(quote! { #pad_name: 0.0 });
431            } else {
432                let count = (padding_needed / 4) as usize;
433                wgsl_fields.push(format!("    {}: array<f32, {}>,", pad_name_str, count));
434                gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
435                to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
436            }
437            field_offset += padding_needed;
438        }
439
440        wgsl_fields.push("    particle_type: u32,".to_string());
441        gpu_struct_fields.push(quote! { particle_type: u32 });
442        to_gpu_conversions.push(quote! { particle_type: 0 });
443        field_offset += 4;
444    }
445
446    // Always inject lifecycle fields: age (f32), alive (u32), scale (f32)
447    // These are always present for particle lifecycle management
448    // age: time since spawn, alive: 0 = dead, 1 = alive, scale: particle size
449    let alive_offset: u32;
450    let scale_offset: u32;
451    {
452        // age: f32 (4-byte aligned)
453        let padding_needed = (4 - (field_offset % 4)) % 4;
454        if padding_needed > 0 {
455            let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
456            let pad_name_str = format!("_pad{}", padding_count);
457            padding_count += 1;
458            let count = (padding_needed / 4) as usize;
459            if count == 1 {
460                wgsl_fields.push(format!("    {}: f32,", pad_name_str));
461                gpu_struct_fields.push(quote! { #pad_name: f32 });
462                to_gpu_conversions.push(quote! { #pad_name: 0.0 });
463            } else {
464                wgsl_fields.push(format!("    {}: array<f32, {}>,", pad_name_str, count));
465                gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
466                to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
467            }
468            field_offset += padding_needed;
469        }
470
471        wgsl_fields.push("    age: f32,".to_string());
472        gpu_struct_fields.push(quote! { age: f32 });
473        to_gpu_conversions.push(quote! { age: 0.0 });
474        field_offset += 4;
475
476        // alive: u32 (4-byte aligned, already aligned after f32)
477        // Record offset before adding the field
478        alive_offset = field_offset;
479        wgsl_fields.push("    alive: u32,".to_string());
480        gpu_struct_fields.push(quote! { alive: u32 });
481        to_gpu_conversions.push(quote! { alive: 1u32 }); // Particles start alive
482        field_offset += 4;
483
484        // scale: f32 (4-byte aligned, already aligned after u32)
485        // Record offset before adding the field
486        scale_offset = field_offset;
487        wgsl_fields.push("    scale: f32,".to_string());
488        gpu_struct_fields.push(quote! { scale: f32 });
489        to_gpu_conversions.push(quote! { scale: 1.0 }); // Default scale of 1.0
490        field_offset += 4;
491    }
492
493    // Ensure struct size is multiple of 16 (vec4 alignment for GPU arrays)
494    let final_padding = (16 - (field_offset % 16)) % 16;
495    if final_padding > 0 {
496        let pad_name = Ident::new(&format!("_pad{}", padding_count), Span::call_site());
497        let pad_name_str = format!("_pad{}", padding_count);
498
499        if final_padding == 4 {
500            wgsl_fields.push(format!("    {}: f32,", pad_name_str));
501            gpu_struct_fields.push(quote! { #pad_name: f32 });
502            to_gpu_conversions.push(quote! { #pad_name: 0.0 });
503        } else {
504            let count = (final_padding / 4) as usize;
505            wgsl_fields.push(format!("    {}: array<f32, {}>,", pad_name_str, count));
506            gpu_struct_fields.push(quote! { #pad_name: [f32; #count] });
507            to_gpu_conversions.push(quote! { #pad_name: [0.0; #count] });
508        }
509    }
510
511    let wgsl_struct = format!("struct Particle {{\n{}\n}}", wgsl_fields.join("\n"));
512
513    let color_field_expr = match color_field {
514        Some(ref name) => quote! { Some(#name) },
515        None => quote! { None },
516    };
517
518    let color_offset_expr = match color_offset {
519        Some(offset) => quote! { Some(#offset) },
520        None => quote! { None },
521    };
522
523    let expanded = quote! {
524        #[repr(C)]
525        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
526        pub struct #gpu_name {
527            #(pub #gpu_struct_fields),*
528        }
529
530        impl rdpe::ParticleTrait for #name {
531            type Gpu = #gpu_name;
532
533            const WGSL_STRUCT: &'static str = #wgsl_struct;
534            const COLOR_FIELD: Option<&'static str> = #color_field_expr;
535            const COLOR_OFFSET: Option<u32> = #color_offset_expr;
536            const ALIVE_OFFSET: u32 = #alive_offset;
537            const SCALE_OFFSET: u32 = #scale_offset;
538
539            fn to_gpu(&self) -> Self::Gpu {
540                #gpu_name {
541                    #(#to_gpu_conversions),*
542                }
543            }
544
545            fn from_gpu(gpu: &Self::Gpu) -> Self {
546                Self {
547                    #(#from_gpu_conversions),*
548                }
549            }
550
551            fn inspect_fields(&self) -> Vec<(&'static str, String)> {
552                vec![
553                    #(#inspect_field_entries),*
554                ]
555            }
556
557            #[cfg(feature = "egui")]
558            fn render_editable_fields(&mut self, ui: &mut egui::Ui) -> bool {
559                let mut modified = false;
560                egui::Grid::new("editable_fields")
561                    .num_columns(2)
562                    .spacing([20.0, 4.0])
563                    .show(ui, |ui| {
564                        #(#editable_field_widgets)*
565                    });
566                modified
567            }
568        }
569    };
570
571    TokenStream::from(expanded)
572}
573
574/// Type metadata for GPU memory layout calculations.
575struct TypeInfo {
576    /// WGSL type name (e.g., "vec3<f32>")
577    wgsl_type: &'static str,
578    /// Rust type for the GPU struct (e.g., `[f32; 3]`)
579    gpu_type: proc_macro2::TokenStream,
580    /// Size in bytes
581    size: u32,
582    /// Required alignment in bytes
583    align: u32,
584}
585
586/// Get type information for a Rust type.
587///
588/// Maps Rust types to their WGSL equivalents and alignment requirements.
589fn rust_type_info(ty: &Type) -> TypeInfo {
590    let type_str = quote!(#ty).to_string().replace(" ", "");
591
592    match type_str.as_str() {
593        "Vec3" | "glam::Vec3" => TypeInfo {
594            wgsl_type: "vec3<f32>",
595            gpu_type: quote! { [f32; 3] },
596            size: 12,
597            align: 16, // vec3 has 16-byte alignment in WGSL!
598        },
599        "Vec2" | "glam::Vec2" => TypeInfo {
600            wgsl_type: "vec2<f32>",
601            gpu_type: quote! { [f32; 2] },
602            size: 8,
603            align: 8,
604        },
605        "Vec4" | "glam::Vec4" => TypeInfo {
606            wgsl_type: "vec4<f32>",
607            gpu_type: quote! { [f32; 4] },
608            size: 16,
609            align: 16,
610        },
611        "f32" => TypeInfo {
612            wgsl_type: "f32",
613            gpu_type: quote! { f32 },
614            size: 4,
615            align: 4,
616        },
617        "u32" => TypeInfo {
618            wgsl_type: "u32",
619            gpu_type: quote! { u32 },
620            size: 4,
621            align: 4,
622        },
623        "i32" => TypeInfo {
624            wgsl_type: "i32",
625            gpu_type: quote! { i32 },
626            size: 4,
627            align: 4,
628        },
629        _ => panic!("Unsupported type in Particle struct: {}", type_str),
630    }
631}
632
633/// Generate code to convert a field from Rust to GPU format.
634///
635/// Vector types need `.to_array()`, scalars are passed through.
636fn generate_conversion(field_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
637    let type_str = quote!(#ty).to_string().replace(" ", "");
638
639    match type_str.as_str() {
640        "Vec3" | "glam::Vec3" | "Vec2" | "glam::Vec2" | "Vec4" | "glam::Vec4" => {
641            quote! { self.#field_name.to_array() }
642        }
643        _ => {
644            quote! { self.#field_name }
645        }
646    }
647}
648
649/// Generate code to convert a field from GPU format back to Rust.
650///
651/// Vector types need `from_array()`, scalars are passed through.
652fn generate_reverse_conversion(field_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
653    let type_str = quote!(#ty).to_string().replace(" ", "");
654
655    match type_str.as_str() {
656        "Vec3" | "glam::Vec3" => {
657            quote! { glam::Vec3::from_array(gpu.#field_name) }
658        }
659        "Vec2" | "glam::Vec2" => {
660            quote! { glam::Vec2::from_array(gpu.#field_name) }
661        }
662        "Vec4" | "glam::Vec4" => {
663            quote! { glam::Vec4::from_array(gpu.#field_name) }
664        }
665        _ => {
666            quote! { gpu.#field_name }
667        }
668    }
669}
670
671/// Generate code to format a field for inspection display.
672///
673/// Produces human-readable formatted strings for the inspector panel.
674fn generate_inspect_format(field_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
675    let type_str = quote!(#ty).to_string().replace(" ", "");
676
677    match type_str.as_str() {
678        "Vec3" | "glam::Vec3" => {
679            quote! {
680                format!("({:.3}, {:.3}, {:.3})", self.#field_name.x, self.#field_name.y, self.#field_name.z)
681            }
682        }
683        "Vec2" | "glam::Vec2" => {
684            quote! {
685                format!("({:.3}, {:.3})", self.#field_name.x, self.#field_name.y)
686            }
687        }
688        "Vec4" | "glam::Vec4" => {
689            quote! {
690                format!("({:.3}, {:.3}, {:.3}, {:.3})", self.#field_name.x, self.#field_name.y, self.#field_name.z, self.#field_name.w)
691            }
692        }
693        "f32" => {
694            quote! { format!("{:.3}", self.#field_name) }
695        }
696        "u32" | "i32" => {
697            quote! { format!("{}", self.#field_name) }
698        }
699        _ => {
700            quote! { format!("{:?}", self.#field_name) }
701        }
702    }
703}
704
705/// Generate editable UI widget code for a field.
706///
707/// Produces egui widget code for editing particle fields in the inspector.
708fn generate_editable_widget(field_name: &Ident, field_name_str: &str, ty: &Type) -> proc_macro2::TokenStream {
709    let type_str = quote!(#ty).to_string().replace(" ", "");
710
711    match type_str.as_str() {
712        "Vec3" | "glam::Vec3" => {
713            quote! {
714                ui.label(#field_name_str);
715                ui.horizontal(|ui| {
716                    if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
717                        modified = true;
718                    }
719                    if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
720                        modified = true;
721                    }
722                    if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
723                        modified = true;
724                    }
725                });
726                ui.end_row();
727            }
728        }
729        "Vec2" | "glam::Vec2" => {
730            quote! {
731                ui.label(#field_name_str);
732                ui.horizontal(|ui| {
733                    if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
734                        modified = true;
735                    }
736                    if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
737                        modified = true;
738                    }
739                });
740                ui.end_row();
741            }
742        }
743        "Vec4" | "glam::Vec4" => {
744            quote! {
745                ui.label(#field_name_str);
746                ui.horizontal(|ui| {
747                    if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
748                        modified = true;
749                    }
750                    if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
751                        modified = true;
752                    }
753                    if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
754                        modified = true;
755                    }
756                    if ui.add(egui::DragValue::new(&mut self.#field_name.w).speed(0.01).prefix("w: ")).changed() {
757                        modified = true;
758                    }
759                });
760                ui.end_row();
761            }
762        }
763        "f32" => {
764            quote! {
765                ui.label(#field_name_str);
766                if ui.add(egui::DragValue::new(&mut self.#field_name).speed(0.01)).changed() {
767                    modified = true;
768                }
769                ui.end_row();
770            }
771        }
772        "u32" => {
773            quote! {
774                ui.label(#field_name_str);
775                let mut val = self.#field_name as i64;
776                if ui.add(egui::DragValue::new(&mut val).speed(1.0)).changed() {
777                    self.#field_name = val.max(0) as u32;
778                    modified = true;
779                }
780                ui.end_row();
781            }
782        }
783        "i32" => {
784            quote! {
785                ui.label(#field_name_str);
786                if ui.add(egui::DragValue::new(&mut self.#field_name).speed(1.0)).changed() {
787                    modified = true;
788                }
789                ui.end_row();
790            }
791        }
792        _ => {
793            // For unknown types, just show as read-only
794            quote! {
795                ui.label(#field_name_str);
796                ui.label(format!("{:?}", self.#field_name));
797                ui.end_row();
798            }
799        }
800    }
801}
802
803/// Generate editable UI widget code for a field (from string type).
804///
805/// Used by MultiParticle derive where we have type as string.
806fn generate_editable_widget_from_string(field_name: &Ident, field_name_str: &str, type_str: &str) -> proc_macro2::TokenStream {
807    match type_str {
808        "Vec3" => {
809            quote! {
810                ui.label(#field_name_str);
811                ui.horizontal(|ui| {
812                    if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
813                        modified = true;
814                    }
815                    if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
816                        modified = true;
817                    }
818                    if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
819                        modified = true;
820                    }
821                });
822                ui.end_row();
823            }
824        }
825        "Vec2" => {
826            quote! {
827                ui.label(#field_name_str);
828                ui.horizontal(|ui| {
829                    if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
830                        modified = true;
831                    }
832                    if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
833                        modified = true;
834                    }
835                });
836                ui.end_row();
837            }
838        }
839        "Vec4" => {
840            quote! {
841                ui.label(#field_name_str);
842                ui.horizontal(|ui| {
843                    if ui.add(egui::DragValue::new(&mut self.#field_name.x).speed(0.01).prefix("x: ")).changed() {
844                        modified = true;
845                    }
846                    if ui.add(egui::DragValue::new(&mut self.#field_name.y).speed(0.01).prefix("y: ")).changed() {
847                        modified = true;
848                    }
849                    if ui.add(egui::DragValue::new(&mut self.#field_name.z).speed(0.01).prefix("z: ")).changed() {
850                        modified = true;
851                    }
852                    if ui.add(egui::DragValue::new(&mut self.#field_name.w).speed(0.01).prefix("w: ")).changed() {
853                        modified = true;
854                    }
855                });
856                ui.end_row();
857            }
858        }
859        "f32" => {
860            quote! {
861                ui.label(#field_name_str);
862                if ui.add(egui::DragValue::new(&mut self.#field_name).speed(0.01)).changed() {
863                    modified = true;
864                }
865                ui.end_row();
866            }
867        }
868        "u32" => {
869            quote! {
870                ui.label(#field_name_str);
871                let mut val = self.#field_name as i64;
872                if ui.add(egui::DragValue::new(&mut val).speed(1.0)).changed() {
873                    self.#field_name = val.max(0) as u32;
874                    modified = true;
875                }
876                ui.end_row();
877            }
878        }
879        "i32" => {
880            quote! {
881                ui.label(#field_name_str);
882                if ui.add(egui::DragValue::new(&mut self.#field_name).speed(1.0)).changed() {
883                    modified = true;
884                }
885                ui.end_row();
886            }
887        }
888        _ => {
889            quote! {
890                ui.label(#field_name_str);
891                ui.label(format!("{:?}", self.#field_name));
892                ui.end_row();
893            }
894        }
895    }
896}
897
898/// Derive macro for multi-particle enums with inline struct definitions.
899///
900/// Generates standalone particle structs AND a unified enum, enabling both
901/// single-type and heterogeneous particle simulations from one definition.
902///
903/// # Overview
904///
905/// `MultiParticle` lets you define multiple particle types inline as struct-like
906/// enum variants. The macro generates:
907///
908/// 1. **Standalone structs** - Each variant becomes its own struct implementing `ParticleTrait`
909/// 2. **Unified enum** - The enum implements `ParticleTrait` with all fields combined
910/// 3. **Rust type constants** - `EnumName::VARIANT` constants for use in typed rules
911/// 4. **WGSL helpers** - Type constants and helper functions for shaders
912///
913/// # Example
914///
915/// ```ignore
916/// use rdpe::prelude::*;
917///
918/// #[derive(MultiParticle, Clone)]
919/// enum Creature {
920///     Boid {
921///         position: Vec3,
922///         velocity: Vec3,
923///         flock_id: u32,
924///     },
925///     Predator {
926///         position: Vec3,
927///         velocity: Vec3,
928///         hunger: f32,
929///         target_id: u32,
930///     },
931/// }
932///
933/// // All three work:
934/// Simulation::<Creature>::new()  // Mixed simulation
935/// Simulation::<Boid>::new()      // Boid-only simulation (uses generated struct)
936/// Simulation::<Predator>::new()  // Predator-only simulation (uses generated struct)
937///
938/// // Creating particles with clean struct-like syntax:
939/// Creature::Boid { position: Vec3::ZERO, velocity: Vec3::ZERO, flock_id: 0 }
940/// Creature::Predator { position: Vec3::ZERO, velocity: Vec3::ZERO, hunger: 1.0, target_id: 0 }
941///
942/// // Using type constants in rules:
943/// Rule::Chase {
944///     self_type: Creature::PREDATOR,
945///     target_type: Creature::BOID,
946///     radius: 0.5,
947///     strength: 3.0,
948/// }
949/// ```
950///
951/// # Requirements
952///
953/// - The enum must also derive `Clone` (for `ParticleTrait` bounds)
954/// - Each variant must use struct-like syntax with named fields
955/// - Each variant must have `position: Vec3` and `velocity: Vec3` fields
956/// - Use `#[color]` attribute on a `Vec3` field for custom particle color
957///
958/// # Generated Code
959///
960/// For an enum `Creature { Boid { ... }, Predator { ... } }`, the macro generates:
961///
962/// ```ignore
963/// // Standalone structs (separate types for single-particle simulations)
964/// struct Boid { position: Vec3, velocity: Vec3, flock_id: u32 }
965/// impl ParticleTrait for Boid { ... }
966///
967/// struct Predator { position: Vec3, velocity: Vec3, hunger: f32, target_id: u32 }
968/// impl ParticleTrait for Predator { ... }
969///
970/// // ParticleTrait on the original enum (for mixed simulations)
971/// impl ParticleTrait for Creature { ... }  // Unified GPU struct with all fields
972/// ```
973///
974/// # WGSL Usage
975///
976/// In custom rules, use the generated constants and helpers:
977///
978/// ```wgsl
979/// // Type constants
980/// const BOID: u32 = 0u;
981/// const PREDATOR: u32 = 1u;
982///
983/// // Helper functions
984/// fn is_boid(p: Particle) -> bool { return p.particle_type == 0u; }
985/// fn is_predator(p: Particle) -> bool { return p.particle_type == 1u; }
986///
987/// // Usage in custom rules
988/// if is_predator(p) {
989///     p.hunger -= uniforms.delta_time * 0.1;
990/// }
991/// ```
992#[proc_macro_derive(MultiParticle, attributes(color))]
993pub fn derive_multi_particle(input: TokenStream) -> TokenStream {
994    let input = parse_macro_input!(input as DeriveInput);
995    let enum_name = &input.ident;
996    let enum_gpu_name = Ident::new(&format!("{}Gpu", enum_name), Span::call_site());
997    let visibility = &input.vis;
998
999    // Parse as enum with struct-like variants
1000    let variants = match &input.data {
1001        Data::Enum(data) => &data.variants,
1002        _ => panic!("MultiParticle derive only supports enums"),
1003    };
1004
1005    // Collect variant info: (name, fields as Vec<(name, type_string, is_color)>)
1006    let mut variant_info: Vec<(Ident, Vec<(Ident, String, bool)>)> = Vec::new();
1007
1008    for variant in variants.iter() {
1009        let variant_name = variant.ident.clone();
1010
1011        let fields = match &variant.fields {
1012            Fields::Named(named) => {
1013                named.named.iter().map(|f| {
1014                    let field_name = f.ident.clone().unwrap();
1015                    let ty = &f.ty;
1016                    let type_str = quote!(#ty).to_string().replace(" ", "");
1017                    let is_color = f.attrs.iter().any(|a| a.path().is_ident("color"));
1018                    (field_name, type_str, is_color)
1019                }).collect::<Vec<_>>()
1020            }
1021            _ => panic!(
1022                "MultiParticle variant '{}' must have named fields (struct-like syntax)",
1023                variant_name
1024            ),
1025        };
1026
1027        // Validate required fields
1028        let has_position = fields.iter().any(|(n, t, _)| n == "position" && (t == "Vec3" || t == "glam::Vec3"));
1029        let has_velocity = fields.iter().any(|(n, t, _)| n == "velocity" && (t == "Vec3" || t == "glam::Vec3"));
1030
1031        if !has_position {
1032            panic!("MultiParticle variant '{}' must have 'position: Vec3' field", variant_name);
1033        }
1034        if !has_velocity {
1035            panic!("MultiParticle variant '{}' must have 'velocity: Vec3' field", variant_name);
1036        }
1037
1038        variant_info.push((variant_name, fields));
1039    }
1040
1041    // ========================================
1042    // Generate standalone structs for each variant
1043    // ========================================
1044    let mut standalone_structs = Vec::new();
1045
1046    for (variant_name, fields) in &variant_info {
1047        let struct_gpu_name = Ident::new(&format!("{}Gpu", variant_name), Span::call_site());
1048
1049        // Build struct fields
1050        let struct_fields: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1051            let ty = rust_type_from_string(type_str);
1052            quote! { pub #name: #ty }
1053        }).collect();
1054
1055        // Build GPU struct using the generate_particle_gpu_struct helper
1056        let (gpu_fields, wgsl_struct, color_field, color_offset, alive_offset, scale_offset) =
1057            generate_particle_gpu_struct(fields, false); // false = include particle_type
1058
1059        let gpu_field_tokens: Vec<_> = gpu_fields.iter().map(|(name, ty)| {
1060            let name_ident = Ident::new(name, Span::call_site());
1061            quote! { pub #name_ident: #ty }
1062        }).collect();
1063
1064        // Build to_gpu conversions
1065        let to_gpu_conversions: Vec<_> = gpu_fields.iter().map(|(name, _ty)| {
1066            let name_ident = Ident::new(name, Span::call_site());
1067            if name.starts_with("_pad") {
1068                // Use Default::default() for all padding (works for both f32 and [f32; N])
1069                quote! { #name_ident: Default::default() }
1070            } else if name == "particle_type" {
1071                quote! { #name_ident: 0 }
1072            } else if name == "age" {
1073                quote! { #name_ident: 0.0 }
1074            } else if name == "alive" {
1075                quote! { #name_ident: 1u32 }
1076            } else if name == "scale" {
1077                quote! { #name_ident: 1.0 }
1078            } else {
1079                // User field - check if it needs to_array
1080                #[allow(clippy::cmp_owned)]
1081                let field_info = fields.iter().find(|(n, _, _)| n.to_string() == *name);
1082                if let Some((_, type_str, _)) = field_info {
1083                    if type_str == "Vec3" || type_str == "Vec2" || type_str == "Vec4" ||
1084                       type_str == "glam::Vec3" || type_str == "glam::Vec2" || type_str == "glam::Vec4" {
1085                        quote! { #name_ident: self.#name_ident.to_array() }
1086                    } else {
1087                        quote! { #name_ident: self.#name_ident }
1088                    }
1089                } else {
1090                    quote! { #name_ident: Default::default() }
1091                }
1092            }
1093        }).collect();
1094
1095        // Build from_gpu conversions for each user field
1096        let from_gpu_conversions: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1097            let name_ident = name;
1098            let type_normalized = type_str.replace("glam::", "");
1099            match type_normalized.as_str() {
1100                "Vec3" => quote! { #name_ident: rdpe::Vec3::from_array(gpu.#name_ident) },
1101                "Vec2" => quote! { #name_ident: rdpe::Vec2::from_array(gpu.#name_ident) },
1102                "Vec4" => quote! { #name_ident: rdpe::Vec4::from_array(gpu.#name_ident) },
1103                _ => quote! { #name_ident: gpu.#name_ident },
1104            }
1105        }).collect();
1106
1107        // Build inspect field entries
1108        let inspect_entries: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1109            let name_str = name.to_string();
1110            let type_normalized = type_str.replace("glam::", "");
1111            match type_normalized.as_str() {
1112                "Vec3" => quote! { (#name_str, format!("({:.3}, {:.3}, {:.3})", self.#name.x, self.#name.y, self.#name.z)) },
1113                "Vec2" => quote! { (#name_str, format!("({:.3}, {:.3})", self.#name.x, self.#name.y)) },
1114                "Vec4" => quote! { (#name_str, format!("({:.3}, {:.3}, {:.3}, {:.3})", self.#name.x, self.#name.y, self.#name.z, self.#name.w)) },
1115                "f32" => quote! { (#name_str, format!("{:.3}", self.#name)) },
1116                "u32" | "i32" => quote! { (#name_str, format!("{}", self.#name)) },
1117                _ => quote! { (#name_str, format!("{:?}", self.#name)) },
1118            }
1119        }).collect();
1120
1121        // Build editable widget entries
1122        let editable_entries: Vec<_> = fields.iter().map(|(name, type_str, _)| {
1123            let name_str = name.to_string();
1124            let type_normalized = type_str.replace("glam::", "");
1125            generate_editable_widget_from_string(name, &name_str, &type_normalized)
1126        }).collect();
1127
1128        let color_field_expr = match &color_field {
1129            Some(name) => quote! { Some(#name) },
1130            None => quote! { None },
1131        };
1132
1133        let color_offset_expr = match color_offset {
1134            Some(offset) => quote! { Some(#offset) },
1135            None => quote! { None },
1136        };
1137
1138        standalone_structs.push(quote! {
1139            /// Auto-generated struct from MultiParticle variant
1140            #[derive(Clone)]
1141            #visibility struct #variant_name {
1142                #(#struct_fields),*
1143            }
1144
1145            #[repr(C)]
1146            #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1147            #visibility struct #struct_gpu_name {
1148                #(#gpu_field_tokens),*
1149            }
1150
1151            impl rdpe::ParticleTrait for #variant_name {
1152                type Gpu = #struct_gpu_name;
1153
1154                const WGSL_STRUCT: &'static str = #wgsl_struct;
1155                const COLOR_FIELD: Option<&'static str> = #color_field_expr;
1156                const COLOR_OFFSET: Option<u32> = #color_offset_expr;
1157                const ALIVE_OFFSET: u32 = #alive_offset;
1158                const SCALE_OFFSET: u32 = #scale_offset;
1159
1160                fn to_gpu(&self) -> Self::Gpu {
1161                    #struct_gpu_name {
1162                        #(#to_gpu_conversions),*
1163                    }
1164                }
1165
1166                fn from_gpu(gpu: &Self::Gpu) -> Self {
1167                    Self {
1168                        #(#from_gpu_conversions),*
1169                    }
1170                }
1171
1172                fn inspect_fields(&self) -> Vec<(&'static str, String)> {
1173                    vec![
1174                        #(#inspect_entries),*
1175                    ]
1176                }
1177
1178                #[cfg(feature = "egui")]
1179                fn render_editable_fields(&mut self, ui: &mut egui::Ui) -> bool {
1180                    let mut modified = false;
1181                    egui::Grid::new("editable_fields")
1182                        .num_columns(2)
1183                        .spacing([20.0, 4.0])
1184                        .show(ui, |ui| {
1185                            #(#editable_entries)*
1186                        });
1187                    modified
1188                }
1189            }
1190        });
1191    }
1192
1193    // ========================================
1194    // Build unified field list for the enum
1195    // ========================================
1196    let mut all_fields: Vec<(String, String, bool)> = vec![
1197        ("position".to_string(), "Vec3".to_string(), false),
1198        ("velocity".to_string(), "Vec3".to_string(), false),
1199    ];
1200
1201    let mut seen_fields: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1202    seen_fields.insert("position".to_string(), "Vec3".to_string());
1203    seen_fields.insert("velocity".to_string(), "Vec3".to_string());
1204
1205    for (variant_name, fields) in &variant_info {
1206        for (fname, ftype, is_color) in fields {
1207            let fname_str = fname.to_string();
1208            let ftype_normalized = ftype.replace("glam::", "");
1209            if fname_str == "position" || fname_str == "velocity" {
1210                continue; // Already added
1211            }
1212            if let Some(existing_type) = seen_fields.get(&fname_str) {
1213                if existing_type != &ftype_normalized {
1214                    panic!(
1215                        "Field '{}' has conflicting types: '{}' in one variant, '{}' in '{}'",
1216                        fname_str, existing_type, ftype_normalized, variant_name
1217                    );
1218                }
1219            } else {
1220                seen_fields.insert(fname_str.clone(), ftype_normalized.clone());
1221                all_fields.push((fname_str, ftype_normalized, *is_color));
1222            }
1223        }
1224    }
1225
1226    // ========================================
1227    // Generate unified GPU struct for enum
1228    // ========================================
1229    let (enum_gpu_fields, enum_wgsl_struct, _, _, enum_alive_offset, enum_scale_offset) =
1230        generate_unified_gpu_struct(&all_fields);
1231
1232    let enum_gpu_field_tokens: Vec<_> = enum_gpu_fields.iter().map(|(name, ty)| {
1233        let name_ident = Ident::new(name, Span::call_site());
1234        quote! { pub #name_ident: #ty }
1235    }).collect();
1236
1237    // Generate EXTRA_WGSL with type constants and helpers
1238    let mut extra_wgsl_lines = Vec::new();
1239    extra_wgsl_lines.push("// MultiParticle type constants".to_string());
1240
1241    for (i, (variant_name, _)) in variant_info.iter().enumerate() {
1242        let const_name = variant_name.to_string().to_uppercase();
1243        extra_wgsl_lines.push(format!("const {}: u32 = {}u;", const_name, i));
1244    }
1245
1246    extra_wgsl_lines.push("".to_string());
1247    extra_wgsl_lines.push("// MultiParticle type helpers".to_string());
1248
1249    for (i, (variant_name, _)) in variant_info.iter().enumerate() {
1250        let fn_name = format!("is_{}", variant_name.to_string().to_lowercase());
1251        extra_wgsl_lines.push(format!(
1252            "fn {}(p: Particle) -> bool {{ return p.particle_type == {}u; }}",
1253            fn_name, i
1254        ));
1255    }
1256
1257    let extra_wgsl = extra_wgsl_lines.join("\n");
1258
1259    // Generate to_gpu() match arms for enum (matching on struct-like variants)
1260    let to_gpu_arms: Vec<_> = variant_info
1261        .iter()
1262        .enumerate()
1263        .map(|(idx, (variant_name, variant_fields))| {
1264            let idx_u32 = idx as u32;
1265
1266            // Generate field bindings for the match pattern
1267            let field_bindings: Vec<_> = variant_fields.iter().map(|(fname, _, _)| {
1268                quote! { #fname }
1269            }).collect();
1270
1271            // Generate field assignments for the GPU struct
1272            let mut field_assignments = Vec::new();
1273
1274            for (fname, ftype, _) in &all_fields {
1275                let field_ident = Ident::new(fname, Span::call_site());
1276
1277                // Check if this variant has this field
1278                #[allow(clippy::cmp_owned)]
1279                let has_field = variant_fields.iter().any(|(vf, _, _)| vf.to_string() == *fname);
1280
1281                if has_field {
1282                    let type_info = type_info_from_string(ftype);
1283                    if type_info.needs_to_array {
1284                        field_assignments.push(quote! { #field_ident: #field_ident.to_array() });
1285                    } else {
1286                        field_assignments.push(quote! { #field_ident: *#field_ident });
1287                    }
1288                } else {
1289                    let zero_val = zero_value_for_type(ftype);
1290                    field_assignments.push(quote! { #field_ident: #zero_val });
1291                }
1292            }
1293
1294            // Particle type and lifecycle
1295            field_assignments.push(quote! { particle_type: #idx_u32 });
1296            field_assignments.push(quote! { age: 0.0 });
1297            field_assignments.push(quote! { alive: 1u32 });
1298            field_assignments.push(quote! { scale: 1.0 });
1299
1300            quote! {
1301                #enum_name::#variant_name { #(#field_bindings),* } => {
1302                    #enum_gpu_name {
1303                        #(#field_assignments,)*
1304                        ..bytemuck::Zeroable::zeroed()
1305                    }
1306                }
1307            }
1308        })
1309        .collect();
1310
1311    // Generate from_gpu() match arms for enum
1312    let from_gpu_arms: Vec<_> = variant_info
1313        .iter()
1314        .enumerate()
1315        .map(|(idx, (variant_name, variant_fields))| {
1316            let idx_u32 = idx as u32;
1317
1318            // Generate field assignments from GPU to enum variant
1319            let field_assignments: Vec<_> = variant_fields.iter().map(|(fname, ftype, _)| {
1320                let type_normalized = ftype.replace("glam::", "");
1321                match type_normalized.as_str() {
1322                    "Vec3" => quote! { #fname: rdpe::Vec3::from_array(gpu.#fname) },
1323                    "Vec2" => quote! { #fname: rdpe::Vec2::from_array(gpu.#fname) },
1324                    "Vec4" => quote! { #fname: rdpe::Vec4::from_array(gpu.#fname) },
1325                    _ => quote! { #fname: gpu.#fname },
1326                }
1327            }).collect();
1328
1329            // Need a default case for the last variant
1330            if idx == variant_info.len() - 1 {
1331                quote! {
1332                    _ => #enum_name::#variant_name { #(#field_assignments),* }
1333                }
1334            } else {
1335                quote! {
1336                    #idx_u32 => #enum_name::#variant_name { #(#field_assignments),* },
1337                }
1338            }
1339        })
1340        .collect();
1341
1342    // Generate inspect_fields() match arms for enum
1343    let inspect_arms: Vec<_> = variant_info
1344        .iter()
1345        .map(|(variant_name, variant_fields)| {
1346            // Generate field bindings for the match pattern
1347            let field_bindings: Vec<_> = variant_fields.iter().map(|(fname, _, _)| {
1348                quote! { #fname }
1349            }).collect();
1350
1351            // Generate inspect entries
1352            let inspect_entries: Vec<_> = variant_fields.iter().map(|(fname, ftype, _)| {
1353                let fname_str = fname.to_string();
1354                let type_normalized = ftype.replace("glam::", "");
1355                match type_normalized.as_str() {
1356                    "Vec3" => quote! { (#fname_str, format!("({:.3}, {:.3}, {:.3})", #fname.x, #fname.y, #fname.z)) },
1357                    "Vec2" => quote! { (#fname_str, format!("({:.3}, {:.3})", #fname.x, #fname.y)) },
1358                    "Vec4" => quote! { (#fname_str, format!("({:.3}, {:.3}, {:.3}, {:.3})", #fname.x, #fname.y, #fname.z, #fname.w)) },
1359                    "f32" => quote! { (#fname_str, format!("{:.3}", #fname)) },
1360                    "u32" | "i32" => quote! { (#fname_str, format!("{}", #fname)) },
1361                    _ => quote! { (#fname_str, format!("{:?}", #fname)) },
1362                }
1363            }).collect();
1364
1365            quote! {
1366                #enum_name::#variant_name { #(#field_bindings),* } => {
1367                    vec![#(#inspect_entries),*]
1368                }
1369            }
1370        })
1371        .collect();
1372
1373    // Generate render_editable_fields() match arms for enum
1374    let editable_arms: Vec<_> = variant_info
1375        .iter()
1376        .map(|(variant_name, variant_fields)| {
1377            // Generate mutable field bindings for the match pattern
1378            let field_bindings: Vec<_> = variant_fields.iter().map(|(fname, _, _)| {
1379                quote! { #fname }
1380            }).collect();
1381
1382            // Generate editable widgets for each field
1383            let editable_widgets: Vec<_> = variant_fields.iter().map(|(fname, ftype, _)| {
1384                let fname_str = fname.to_string();
1385                let type_normalized = ftype.replace("glam::", "");
1386                match type_normalized.as_str() {
1387                    "Vec3" => quote! {
1388                        ui.label(#fname_str);
1389                        ui.horizontal(|ui| {
1390                            if ui.add(egui::DragValue::new(&mut #fname.x).speed(0.01).prefix("x: ")).changed() {
1391                                modified = true;
1392                            }
1393                            if ui.add(egui::DragValue::new(&mut #fname.y).speed(0.01).prefix("y: ")).changed() {
1394                                modified = true;
1395                            }
1396                            if ui.add(egui::DragValue::new(&mut #fname.z).speed(0.01).prefix("z: ")).changed() {
1397                                modified = true;
1398                            }
1399                        });
1400                        ui.end_row();
1401                    },
1402                    "Vec2" => quote! {
1403                        ui.label(#fname_str);
1404                        ui.horizontal(|ui| {
1405                            if ui.add(egui::DragValue::new(&mut #fname.x).speed(0.01).prefix("x: ")).changed() {
1406                                modified = true;
1407                            }
1408                            if ui.add(egui::DragValue::new(&mut #fname.y).speed(0.01).prefix("y: ")).changed() {
1409                                modified = true;
1410                            }
1411                        });
1412                        ui.end_row();
1413                    },
1414                    "Vec4" => quote! {
1415                        ui.label(#fname_str);
1416                        ui.horizontal(|ui| {
1417                            if ui.add(egui::DragValue::new(&mut #fname.x).speed(0.01).prefix("x: ")).changed() {
1418                                modified = true;
1419                            }
1420                            if ui.add(egui::DragValue::new(&mut #fname.y).speed(0.01).prefix("y: ")).changed() {
1421                                modified = true;
1422                            }
1423                            if ui.add(egui::DragValue::new(&mut #fname.z).speed(0.01).prefix("z: ")).changed() {
1424                                modified = true;
1425                            }
1426                            if ui.add(egui::DragValue::new(&mut #fname.w).speed(0.01).prefix("w: ")).changed() {
1427                                modified = true;
1428                            }
1429                        });
1430                        ui.end_row();
1431                    },
1432                    "f32" => quote! {
1433                        ui.label(#fname_str);
1434                        if ui.add(egui::DragValue::new(#fname).speed(0.01)).changed() {
1435                            modified = true;
1436                        }
1437                        ui.end_row();
1438                    },
1439                    "u32" => quote! {
1440                        ui.label(#fname_str);
1441                        let mut val = *#fname as i64;
1442                        if ui.add(egui::DragValue::new(&mut val).speed(1.0)).changed() {
1443                            *#fname = val.max(0) as u32;
1444                            modified = true;
1445                        }
1446                        ui.end_row();
1447                    },
1448                    "i32" => quote! {
1449                        ui.label(#fname_str);
1450                        if ui.add(egui::DragValue::new(#fname).speed(1.0)).changed() {
1451                            modified = true;
1452                        }
1453                        ui.end_row();
1454                    },
1455                    _ => quote! {
1456                        ui.label(#fname_str);
1457                        ui.label(format!("{:?}", #fname));
1458                        ui.end_row();
1459                    },
1460                }
1461            }).collect();
1462
1463            quote! {
1464                #enum_name::#variant_name { #(#field_bindings),* } => {
1465                    egui::Grid::new("editable_fields")
1466                        .num_columns(2)
1467                        .spacing([20.0, 4.0])
1468                        .show(ui, |ui| {
1469                            #(#editable_widgets)*
1470                        });
1471                }
1472            }
1473        })
1474        .collect();
1475
1476    // Generate type ID constants for the enum
1477    let type_constants: Vec<_> = variant_info
1478        .iter()
1479        .enumerate()
1480        .map(|(idx, (variant_name, _))| {
1481            let const_name = Ident::new(
1482                &variant_name.to_string().to_uppercase(),
1483                Span::call_site(),
1484            );
1485            let idx_u32 = idx as u32;
1486            quote! {
1487                /// Type ID for use in typed rules (Chase, Evade, Typed, etc.)
1488                pub const #const_name: u32 = #idx_u32;
1489            }
1490        })
1491        .collect();
1492
1493    let expanded = quote! {
1494        // Standalone structs with full Particle implementations
1495        #(#standalone_structs)*
1496
1497        // Type ID constants for the enum
1498        impl #enum_name {
1499            #(#type_constants)*
1500        }
1501
1502        // Unified GPU struct for the enum (we don't re-declare the enum itself!)
1503        #[repr(C)]
1504        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1505        #visibility struct #enum_gpu_name {
1506            #(#enum_gpu_field_tokens),*
1507        }
1508
1509        impl rdpe::ParticleTrait for #enum_name {
1510            type Gpu = #enum_gpu_name;
1511
1512            const WGSL_STRUCT: &'static str = #enum_wgsl_struct;
1513            const COLOR_FIELD: Option<&'static str> = None;
1514            const COLOR_OFFSET: Option<u32> = None;
1515            const ALIVE_OFFSET: u32 = #enum_alive_offset;
1516            const SCALE_OFFSET: u32 = #enum_scale_offset;
1517            const EXTRA_WGSL: &'static str = #extra_wgsl;
1518
1519            fn to_gpu(&self) -> Self::Gpu {
1520                match self {
1521                    #(#to_gpu_arms)*
1522                }
1523            }
1524
1525            fn from_gpu(gpu: &Self::Gpu) -> Self {
1526                match gpu.particle_type {
1527                    #(#from_gpu_arms)*
1528                }
1529            }
1530
1531            fn inspect_fields(&self) -> Vec<(&'static str, String)> {
1532                match self {
1533                    #(#inspect_arms)*
1534                }
1535            }
1536
1537            #[cfg(feature = "egui")]
1538            fn render_editable_fields(&mut self, ui: &mut egui::Ui) -> bool {
1539                let mut modified = false;
1540                match self {
1541                    #(#editable_arms)*
1542                }
1543                modified
1544            }
1545        }
1546    };
1547
1548    TokenStream::from(expanded)
1549}
1550
1551/// Generate a Rust type token from a type string
1552fn rust_type_from_string(ty: &str) -> proc_macro2::TokenStream {
1553    match ty.replace("glam::", "").as_str() {
1554        "Vec3" => quote! { rdpe::Vec3 },
1555        "Vec2" => quote! { rdpe::Vec2 },
1556        "Vec4" => quote! { rdpe::Vec4 },
1557        "f32" => quote! { f32 },
1558        "u32" => quote! { u32 },
1559        "i32" => quote! { i32 },
1560        _ => panic!("Unsupported type: {}", ty),
1561    }
1562}
1563
1564/// Generate GPU struct fields, WGSL, and offsets for a single particle type
1565fn generate_particle_gpu_struct(
1566    fields: &[(Ident, String, bool)],
1567    _is_standalone: bool,
1568) -> (Vec<(String, proc_macro2::TokenStream)>, String, Option<String>, Option<u32>, u32, u32) {
1569    let mut gpu_fields: Vec<(String, proc_macro2::TokenStream)> = Vec::new();
1570    let mut wgsl_lines = Vec::new();
1571    let mut field_offset = 0u32;
1572    let mut padding_count = 0u32;
1573    let mut color_field: Option<String> = None;
1574    let mut color_offset: Option<u32> = None;
1575
1576    for (field_name, type_str, is_color) in fields {
1577        let type_info = type_info_from_string(&type_str.replace("glam::", ""));
1578
1579        // Add padding if needed
1580        let padding_needed = (type_info.align - (field_offset % type_info.align)) % type_info.align;
1581        if padding_needed > 0 {
1582            let pad_name = format!("_pad{}", padding_count);
1583            padding_count += 1;
1584
1585            if padding_needed == 4 {
1586                wgsl_lines.push(format!("    {}: f32,", pad_name));
1587                gpu_fields.push((pad_name, quote! { f32 }));
1588            } else {
1589                let count = (padding_needed / 4) as usize;
1590                wgsl_lines.push(format!("    {}: array<f32, {}>,", pad_name, count));
1591                gpu_fields.push((pad_name, quote! { [f32; #count] }));
1592            }
1593            field_offset += padding_needed;
1594        }
1595
1596        if *is_color {
1597            color_field = Some(field_name.to_string());
1598            color_offset = Some(field_offset);
1599        }
1600
1601        wgsl_lines.push(format!("    {}: {},", field_name, type_info.wgsl_type));
1602        gpu_fields.push((field_name.to_string(), type_info.gpu_type.clone()));
1603        field_offset += type_info.size;
1604    }
1605
1606    // Add particle_type
1607    {
1608        let padding_needed = (4 - (field_offset % 4)) % 4;
1609        if padding_needed > 0 {
1610            let pad_name = format!("_pad{}", padding_count);
1611            padding_count += 1;
1612            let count = (padding_needed / 4) as usize;
1613            if count == 1 {
1614                wgsl_lines.push(format!("    {}: f32,", pad_name));
1615                gpu_fields.push((pad_name, quote! { f32 }));
1616            } else {
1617                wgsl_lines.push(format!("    {}: array<f32, {}>,", pad_name, count));
1618                gpu_fields.push((pad_name, quote! { [f32; #count] }));
1619            }
1620            field_offset += padding_needed;
1621        }
1622
1623        wgsl_lines.push("    particle_type: u32,".to_string());
1624        gpu_fields.push(("particle_type".to_string(), quote! { u32 }));
1625        field_offset += 4;
1626    }
1627
1628    // Add lifecycle fields
1629    wgsl_lines.push("    age: f32,".to_string());
1630    gpu_fields.push(("age".to_string(), quote! { f32 }));
1631    field_offset += 4;
1632
1633    let alive_offset = field_offset;
1634    wgsl_lines.push("    alive: u32,".to_string());
1635    gpu_fields.push(("alive".to_string(), quote! { u32 }));
1636    field_offset += 4;
1637
1638    let scale_offset = field_offset;
1639    wgsl_lines.push("    scale: f32,".to_string());
1640    gpu_fields.push(("scale".to_string(), quote! { f32 }));
1641    field_offset += 4;
1642
1643    // Final padding
1644    let final_padding = (16 - (field_offset % 16)) % 16;
1645    if final_padding > 0 {
1646        let pad_name = format!("_pad{}", padding_count);
1647        if final_padding == 4 {
1648            wgsl_lines.push(format!("    {}: f32,", pad_name));
1649            gpu_fields.push((pad_name, quote! { f32 }));
1650        } else {
1651            let count = (final_padding / 4) as usize;
1652            wgsl_lines.push(format!("    {}: array<f32, {}>,", pad_name, count));
1653            gpu_fields.push((pad_name, quote! { [f32; #count] }));
1654        }
1655    }
1656
1657    let wgsl_struct = format!("struct Particle {{\n{}\n}}", wgsl_lines.join("\n"));
1658
1659    (gpu_fields, wgsl_struct, color_field, color_offset, alive_offset, scale_offset)
1660}
1661
1662/// Generate unified GPU struct for the enum (containing all fields from all variants)
1663fn generate_unified_gpu_struct(
1664    all_fields: &[(String, String, bool)],
1665) -> (Vec<(String, proc_macro2::TokenStream)>, String, Option<String>, Option<u32>, u32, u32) {
1666    let mut gpu_fields: Vec<(String, proc_macro2::TokenStream)> = Vec::new();
1667    let mut wgsl_lines = Vec::new();
1668    let mut field_offset = 0u32;
1669    let mut padding_count = 0u32;
1670
1671    for (field_name, type_str, _) in all_fields {
1672        let type_info = type_info_from_string(type_str);
1673
1674        // Add padding if needed
1675        let padding_needed = (type_info.align - (field_offset % type_info.align)) % type_info.align;
1676        if padding_needed > 0 {
1677            let pad_name = format!("_pad{}", padding_count);
1678            padding_count += 1;
1679
1680            if padding_needed == 4 {
1681                wgsl_lines.push(format!("    {}: f32,", pad_name));
1682                gpu_fields.push((pad_name, quote! { f32 }));
1683            } else {
1684                let count = (padding_needed / 4) as usize;
1685                wgsl_lines.push(format!("    {}: array<f32, {}>,", pad_name, count));
1686                gpu_fields.push((pad_name, quote! { [f32; #count] }));
1687            }
1688            field_offset += padding_needed;
1689        }
1690
1691        wgsl_lines.push(format!("    {}: {},", field_name, type_info.wgsl_type));
1692        gpu_fields.push((field_name.clone(), type_info.gpu_type.clone()));
1693        field_offset += type_info.size;
1694    }
1695
1696    // Add particle_type
1697    {
1698        let padding_needed = (4 - (field_offset % 4)) % 4;
1699        if padding_needed > 0 {
1700            let pad_name = format!("_pad{}", padding_count);
1701            padding_count += 1;
1702            let count = (padding_needed / 4) as usize;
1703            if count == 1 {
1704                wgsl_lines.push(format!("    {}: f32,", pad_name));
1705                gpu_fields.push((pad_name, quote! { f32 }));
1706            } else {
1707                wgsl_lines.push(format!("    {}: array<f32, {}>,", pad_name, count));
1708                gpu_fields.push((pad_name, quote! { [f32; #count] }));
1709            }
1710            field_offset += padding_needed;
1711        }
1712
1713        wgsl_lines.push("    particle_type: u32,".to_string());
1714        gpu_fields.push(("particle_type".to_string(), quote! { u32 }));
1715        field_offset += 4;
1716    }
1717
1718    // Add lifecycle fields
1719    wgsl_lines.push("    age: f32,".to_string());
1720    gpu_fields.push(("age".to_string(), quote! { f32 }));
1721    field_offset += 4;
1722
1723    let alive_offset = field_offset;
1724    wgsl_lines.push("    alive: u32,".to_string());
1725    gpu_fields.push(("alive".to_string(), quote! { u32 }));
1726    field_offset += 4;
1727
1728    let scale_offset = field_offset;
1729    wgsl_lines.push("    scale: f32,".to_string());
1730    gpu_fields.push(("scale".to_string(), quote! { f32 }));
1731    field_offset += 4;
1732
1733    // Final padding
1734    let final_padding = (16 - (field_offset % 16)) % 16;
1735    if final_padding > 0 {
1736        let pad_name = format!("_pad{}", padding_count);
1737        if final_padding == 4 {
1738            wgsl_lines.push(format!("    {}: f32,", pad_name));
1739            gpu_fields.push((pad_name, quote! { f32 }));
1740        } else {
1741            let count = (final_padding / 4) as usize;
1742            wgsl_lines.push(format!("    {}: array<f32, {}>,", pad_name, count));
1743            gpu_fields.push((pad_name, quote! { [f32; #count] }));
1744        }
1745    }
1746
1747    let wgsl_struct = format!("struct Particle {{\n{}\n}}", wgsl_lines.join("\n"));
1748
1749    (gpu_fields, wgsl_struct, None, None, alive_offset, scale_offset)
1750}
1751
1752/// Type info from string for MultiParticle macro
1753struct MultiTypeInfo {
1754    wgsl_type: &'static str,
1755    gpu_type: proc_macro2::TokenStream,
1756    size: u32,
1757    align: u32,
1758    needs_to_array: bool,
1759}
1760
1761fn type_info_from_string(ty: &str) -> MultiTypeInfo {
1762    match ty {
1763        "Vec3" => MultiTypeInfo {
1764            wgsl_type: "vec3<f32>",
1765            gpu_type: quote! { [f32; 3] },
1766            size: 12,
1767            align: 16,
1768            needs_to_array: true,
1769        },
1770        "Vec2" => MultiTypeInfo {
1771            wgsl_type: "vec2<f32>",
1772            gpu_type: quote! { [f32; 2] },
1773            size: 8,
1774            align: 8,
1775            needs_to_array: true,
1776        },
1777        "Vec4" => MultiTypeInfo {
1778            wgsl_type: "vec4<f32>",
1779            gpu_type: quote! { [f32; 4] },
1780            size: 16,
1781            align: 16,
1782            needs_to_array: true,
1783        },
1784        "f32" => MultiTypeInfo {
1785            wgsl_type: "f32",
1786            gpu_type: quote! { f32 },
1787            size: 4,
1788            align: 4,
1789            needs_to_array: false,
1790        },
1791        "u32" => MultiTypeInfo {
1792            wgsl_type: "u32",
1793            gpu_type: quote! { u32 },
1794            size: 4,
1795            align: 4,
1796            needs_to_array: false,
1797        },
1798        "i32" => MultiTypeInfo {
1799            wgsl_type: "i32",
1800            gpu_type: quote! { i32 },
1801            size: 4,
1802            align: 4,
1803            needs_to_array: false,
1804        },
1805        _ => panic!("Unsupported type in MultiParticle: {}", ty),
1806    }
1807}
1808
1809fn zero_value_for_type(ty: &str) -> proc_macro2::TokenStream {
1810    match ty {
1811        "Vec3" => quote! { [0.0, 0.0, 0.0] },
1812        "Vec2" => quote! { [0.0, 0.0] },
1813        "Vec4" => quote! { [0.0, 0.0, 0.0, 0.0] },
1814        "f32" => quote! { 0.0 },
1815        "u32" => quote! { 0 },
1816        "i32" => quote! { 0 },
1817        _ => quote! { Default::default() },
1818    }
1819}