Skip to main content

dreamwell_engine/physics/
emitter.rs

1use super::behaviors::BehaviorBlock;
2use super::classes::SimulationMode;
3use super::collision::CollisionPolicy;
4use super::force_fields::ForceBinding;
5use super::promotion::PromotionPolicy;
6use super::render_policy::RenderPolicy;
7use super::semantic_binding::SemanticBinding;
8use serde::{Deserialize, Serialize};
9
10pub type EmitterId = u64;
11
12/// Spawn shape — spatial distribution for particle emission.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub enum SpawnShape {
15    #[default]
16    Point,
17    Sphere {
18        radius: f32,
19    },
20    Box {
21        extents: [f32; 3],
22    },
23    Cone {
24        radius: f32,
25        angle_radians: f32,
26    },
27    Ring {
28        radius: f32,
29        thickness: f32,
30    },
31    Line {
32        length: f32,
33    },
34    MeshSurface {
35        mesh_asset: String,
36    },
37    VolumeTexture {
38        texture_asset: String,
39    },
40}
41
42/// Spawn mode — emission trigger strategy.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum SpawnMode {
45    Continuous,
46    Burst,
47    EventDriven,
48    Chained,
49}
50
51/// Spawn policy — how particles are emitted.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SpawnPolicy {
54    pub mode: SpawnMode,
55    pub rate_per_sec: f32,
56    pub burst_count: u32,
57    pub max_live: u32,
58    pub warmup_seconds: f32,
59    pub looped: bool,
60    pub seed: u64,
61    pub density_scale_lod: bool,
62}
63
64impl Default for SpawnPolicy {
65    fn default() -> Self {
66        Self {
67            mode: SpawnMode::Continuous,
68            rate_per_sec: 50.0,
69            burst_count: 0,
70            max_live: 1000,
71            warmup_seconds: 0.0,
72            looped: true,
73            seed: 0,
74            density_scale_lod: false,
75        }
76    }
77}
78
79/// Initial velocity distribution.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum VelocityInit {
82    Constant([f32; 3]),
83    RandomRange {
84        min: [f32; 3],
85        max: [f32; 3],
86    },
87    ImpulseFromEventNormal {
88        scale: f32,
89    },
90    ConeDirection {
91        direction: [f32; 3],
92        spread_angle: f32,
93        speed: f32,
94    },
95}
96
97/// Init policy — particle initialization parameters.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct InitPolicy {
100    pub velocity: VelocityInit,
101    pub angular_velocity_range: Option<([f32; 3], [f32; 3])>,
102    pub color: [f32; 4],
103    pub color_random: bool,
104    pub scale_range: ([f32; 2], [f32; 2]),
105    pub mass_range: (f32, f32),
106}
107
108impl Default for InitPolicy {
109    fn default() -> Self {
110        Self {
111            velocity: VelocityInit::RandomRange {
112                min: [-1.0, 1.0, -1.0],
113                max: [1.0, 3.0, 1.0],
114            },
115            angular_velocity_range: None,
116            color: [1.0, 1.0, 1.0, 1.0],
117            color_random: false,
118            scale_range: ([0.05, 0.05], [0.15, 0.15]),
119            mass_range: (1.0, 1.0),
120        }
121    }
122}
123
124/// Lifetime policy.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct LifetimePolicy {
127    pub seconds: f32,
128    pub random_range: Option<(f32, f32)>,
129}
130
131impl Default for LifetimePolicy {
132    fn default() -> Self {
133        Self {
134            seconds: 5.0,
135            random_range: None,
136        }
137    }
138}
139
140/// Noise policy for turbulence.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct NoisePolicy {
143    pub frequency: f32,
144    pub amplitude: f32,
145    pub octaves: u32,
146}
147
148/// Simulation policy — per-frame update rules.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SimPolicy {
151    pub mode: SimulationMode,
152    pub gravity: [f32; 3],
153    pub drag: f32,
154    pub damping: f32,
155    pub noise: Option<NoisePolicy>,
156    pub forces: Vec<ForceBinding>,
157    pub lifetime: LifetimePolicy,
158    pub behaviors: Vec<BehaviorBlock>,
159}
160
161impl Default for SimPolicy {
162    fn default() -> Self {
163        Self {
164            mode: SimulationMode::Full,
165            gravity: [0.0, -9.81, 0.0],
166            drag: 0.05,
167            damping: 0.01,
168            noise: None,
169            forces: Vec::new(),
170            lifetime: LifetimePolicy::default(),
171            behaviors: Vec::new(),
172        }
173    }
174}
175
176/// Budget policy — GPU resource limits for this emitter.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct BudgetPolicy {
179    pub max_gpu_particles: u32,
180    pub importance: String,
181    pub offscreen_mode: SimulationMode,
182}
183
184impl Default for BudgetPolicy {
185    fn default() -> Self {
186        Self {
187            max_gpu_particles: 1000,
188            importance: "Normal".into(),
189            offscreen_mode: SimulationMode::Simplified,
190        }
191    }
192}
193
194/// Replication policy.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ReplicationPolicy {
197    pub class: super::classes::ReplicationClass,
198}
199
200impl Default for ReplicationPolicy {
201    fn default() -> Self {
202        Self {
203            class: super::classes::ReplicationClass::None,
204        }
205    }
206}
207
208/// Emitter descriptor — complete definition of a particle emitter.
209#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210pub struct EmitterDescriptor {
211    pub id: String,
212    pub label: String,
213    pub shape: SpawnShape,
214    pub spawn: SpawnPolicy,
215    pub initialize: InitPolicy,
216    pub simulate: SimPolicy,
217    pub collision: CollisionPolicy,
218    pub render: RenderPolicy,
219    pub promotion: PromotionPolicy,
220    pub semantics: SemanticBinding,
221    pub budgets: BudgetPolicy,
222    pub replication: ReplicationPolicy,
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn emitter_descriptor_default() {
231        let e = EmitterDescriptor::default();
232        assert!(e.id.is_empty());
233        assert_eq!(e.spawn.rate_per_sec, 50.0);
234    }
235
236    #[test]
237    fn emitter_descriptor_serde_roundtrip() {
238        let mut e = EmitterDescriptor::default();
239        e.id = "test_emitter".into();
240        e.label = "Test Emitter".into();
241        e.shape = SpawnShape::Sphere { radius: 2.0 };
242        let json = serde_json::to_string(&e).unwrap();
243        let restored: EmitterDescriptor = serde_json::from_str(&json).unwrap();
244        assert_eq!(restored.id, "test_emitter");
245    }
246
247    #[test]
248    fn spawn_shape_variants() {
249        let shapes = vec![
250            SpawnShape::Point,
251            SpawnShape::Sphere { radius: 1.0 },
252            SpawnShape::Box {
253                extents: [1.0, 2.0, 3.0],
254            },
255            SpawnShape::Cone {
256                radius: 1.0,
257                angle_radians: 0.5,
258            },
259            SpawnShape::Ring {
260                radius: 2.0,
261                thickness: 0.1,
262            },
263            SpawnShape::Line { length: 5.0 },
264        ];
265        for shape in shapes {
266            let json = serde_json::to_string(&shape).unwrap();
267            let _: SpawnShape = serde_json::from_str(&json).unwrap();
268        }
269    }
270}