Skip to main content

proof_engine/editor/
particle_system_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7// CURVE SYSTEM
8// ============================================================
9
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub enum CurveWrapMode { Clamp, Loop, PingPong }
12
13#[derive(Clone, Copy, Debug)]
14pub struct CurveKey {
15    pub time: f32, pub value: f32, pub in_tangent: f32, pub out_tangent: f32,
16}
17impl CurveKey {
18    pub fn new(t: f32, v: f32) -> Self { Self { time: t, value: v, in_tangent: 0.0, out_tangent: 0.0 } }
19    pub fn with_tangents(t: f32, v: f32, i: f32, o: f32) -> Self { Self { time: t, value: v, in_tangent: i, out_tangent: o } }
20}
21
22#[derive(Clone, Debug)]
23pub struct FloatCurve { pub keys: Vec<CurveKey>, pub wrap_mode: CurveWrapMode }
24impl FloatCurve {
25    pub fn new() -> Self { Self { keys: Vec::new(), wrap_mode: CurveWrapMode::Clamp } }
26    pub fn constant(v: f32) -> Self {
27        let mut c = Self::new(); c.keys.push(CurveKey::new(0.0, v)); c.keys.push(CurveKey::new(1.0, v)); c
28    }
29    pub fn linear(s: f32, e: f32) -> Self {
30        let mut c = Self::new(); let sl = e - s;
31        c.keys.push(CurveKey::with_tangents(0.0, s, sl, sl));
32        c.keys.push(CurveKey::with_tangents(1.0, e, sl, sl)); c
33    }
34    pub fn add_key(&mut self, k: CurveKey) {
35        self.keys.push(k);
36        self.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
37    }
38    fn wrap_t(&self, t: f32) -> f32 {
39        if self.keys.is_empty() { return 0.0; }
40        let t0 = self.keys.first().unwrap().time;
41        let t1 = self.keys.last().unwrap().time;
42        let span = t1 - t0;
43        if span <= 0.0 { return t0; }
44        match self.wrap_mode {
45            CurveWrapMode::Clamp => t.clamp(t0, t1),
46            CurveWrapMode::Loop => { let r = (t - t0) % span; t0 + if r < 0.0 { r + span } else { r } }
47            CurveWrapMode::PingPong => {
48                let r = ((t - t0) / span).abs();
49                let i = r as u32;
50                let frac = r - i as f32;
51                t0 + span * if i % 2 == 0 { frac } else { 1.0 - frac }
52            }
53        }
54    }
55    pub fn evaluate(&self, t: f32) -> f32 {
56        if self.keys.is_empty() { return 0.0; }
57        if self.keys.len() == 1 { return self.keys[0].value; }
58        let t = self.wrap_t(t);
59        if t <= self.keys.first().unwrap().time { return self.keys.first().unwrap().value; }
60        if t >= self.keys.last().unwrap().time { return self.keys.last().unwrap().value; }
61        let idx = self.keys.partition_point(|k| k.time <= t).saturating_sub(1);
62        let idx = idx.min(self.keys.len() - 2);
63        let k0 = &self.keys[idx];
64        let k1 = &self.keys[idx + 1];
65        let dt = k1.time - k0.time;
66        if dt <= 0.0 { return k0.value; }
67        let u = (t - k0.time) / dt;
68        let u2 = u * u; let u3 = u2 * u;
69        let h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
70        let h10 = u3 - 2.0 * u2 + u;
71        let h01 = -2.0 * u3 + 3.0 * u2;
72        let h11 = u3 - u2;
73        h00 * k0.value + h10 * dt * k0.out_tangent + h01 * k1.value + h11 * dt * k1.in_tangent
74    }
75    pub fn evaluate_normalized(&self, t: f32) -> f32 { self.evaluate(t.clamp(0.0, 1.0)) }
76}
77
78// ============================================================
79// COLOR GRADIENT
80// ============================================================
81
82#[derive(Clone, Copy, Debug)]
83pub struct GradientKey { pub time: f32, pub color: Vec4 }
84impl GradientKey {
85    pub fn new(time: f32, color: Vec4) -> Self { Self { time, color } }
86}
87
88#[derive(Clone, Debug)]
89pub struct ColorGradient { pub keys: Vec<GradientKey> }
90impl ColorGradient {
91    pub fn new() -> Self { Self { keys: Vec::new() } }
92    pub fn white() -> Self {
93        let mut g = Self::new();
94        g.keys.push(GradientKey::new(0.0, Vec4::ONE));
95        g.keys.push(GradientKey::new(1.0, Vec4::ONE));
96        g
97    }
98    pub fn add_key(&mut self, k: GradientKey) {
99        self.keys.push(k);
100        self.keys.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
101    }
102    pub fn evaluate(&self, t: f32) -> Vec4 {
103        if self.keys.is_empty() { return Vec4::ONE; }
104        if self.keys.len() == 1 { return self.keys[0].color; }
105        let t = t.clamp(0.0, 1.0);
106        if t <= self.keys.first().unwrap().time { return self.keys.first().unwrap().color; }
107        if t >= self.keys.last().unwrap().time { return self.keys.last().unwrap().color; }
108        let idx = self.keys.partition_point(|k| k.time <= t).saturating_sub(1);
109        let idx = idx.min(self.keys.len() - 2);
110        let k0 = &self.keys[idx];
111        let k1 = &self.keys[idx + 1];
112        let dt = k1.time - k0.time;
113        if dt <= 0.0 { return k0.color; }
114        let u = (t - k0.time) / dt;
115        k0.color.lerp(k1.color, u)
116    }
117}
118
119// ============================================================
120// SIMPLE RNG (LCG)
121// ============================================================
122
123#[derive(Clone, Debug)]
124pub struct SimpleRng { pub state: u64 }
125impl SimpleRng {
126    pub fn new(seed: u64) -> Self { Self { state: seed ^ 0x6c62272e07bb0142 } }
127    pub fn next_u64(&mut self) -> u64 {
128        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
129        self.state
130    }
131    pub fn next_f32(&mut self) -> f32 { (self.next_u64() >> 33) as f32 / (u32::MAX as f32) }
132    pub fn next_f32_range(&mut self, lo: f32, hi: f32) -> f32 { lo + self.next_f32() * (hi - lo) }
133    pub fn next_unit_vec3(&mut self) -> Vec3 {
134        loop {
135            let x = self.next_f32_range(-1.0, 1.0);
136            let y = self.next_f32_range(-1.0, 1.0);
137            let z = self.next_f32_range(-1.0, 1.0);
138            let v = Vec3::new(x, y, z);
139            if v.length_squared() <= 1.0 && v.length_squared() > 0.0001 {
140                return v.normalize();
141            }
142        }
143    }
144    pub fn next_unit_vec2(&mut self) -> Vec2 {
145        let angle = self.next_f32() * std::f32::consts::TAU;
146        Vec2::new(angle.cos(), angle.sin())
147    }
148    pub fn next_vec3_in_sphere(&mut self, radius: f32) -> Vec3 {
149        self.next_unit_vec3() * (self.next_f32().cbrt() * radius)
150    }
151    pub fn next_bool(&mut self) -> bool { self.next_u64() & 1 == 0 }
152}
153
154// ============================================================
155// EMITTER SHAPES
156// ============================================================
157
158#[derive(Clone, Debug, PartialEq)]
159pub enum EmitterShape {
160    Point,
161    Sphere { radius: f32, emit_from_shell: bool },
162    Hemisphere { radius: f32, emit_from_shell: bool },
163    Box { half_extents: Vec3, emit_from_shell: bool },
164    Cone { radius: f32, angle_deg: f32, length: f32 },
165    Ring { radius: f32, tube_radius: f32 },
166    Disk { radius: f32 },
167    Line { start: Vec3, end: Vec3 },
168    Trail { points: Vec<Vec3> },
169    Ribbon { points: Vec<Vec3>, width: f32 },
170    Burst { radius: f32, count: u32 },
171    Vortex { radius: f32, height: f32, twist: f32 },
172    Mesh { vertex_count: u32, surface_area: f32 },
173    Skinned { bone_count: u32 },
174}
175
176impl EmitterShape {
177    pub fn sample_position(&self, rng: &mut SimpleRng) -> (Vec3, Vec3) {
178        match self {
179            EmitterShape::Point => (Vec3::ZERO, Vec3::Y),
180            EmitterShape::Sphere { radius, emit_from_shell } => {
181                if *emit_from_shell {
182                    let n = rng.next_unit_vec3();
183                    (n * *radius, n)
184                } else {
185                    let p = rng.next_vec3_in_sphere(*radius);
186                    (p, if p.length_squared() > 0.0 { p.normalize() } else { Vec3::Y })
187                }
188            }
189            EmitterShape::Hemisphere { radius, emit_from_shell } => {
190                if *emit_from_shell {
191                    loop {
192                        let n = rng.next_unit_vec3();
193                        if n.y >= 0.0 { return (n * *radius, n); }
194                    }
195                } else {
196                    loop {
197                        let p = rng.next_vec3_in_sphere(*radius);
198                        if p.y >= 0.0 {
199                            let n = if p.length_squared() > 0.0 { p.normalize() } else { Vec3::Y };
200                            return (p, n);
201                        }
202                    }
203                }
204            }
205            EmitterShape::Box { half_extents, emit_from_shell } => {
206                if *emit_from_shell {
207                    let face = (rng.next_f32() * 6.0) as u32;
208                    let u = rng.next_f32_range(-1.0, 1.0);
209                    let v = rng.next_f32_range(-1.0, 1.0);
210                    let (pos, normal) = match face {
211                        0 => (Vec3::new(half_extents.x, u * half_extents.y, v * half_extents.z), Vec3::X),
212                        1 => (Vec3::new(-half_extents.x, u * half_extents.y, v * half_extents.z), Vec3::NEG_X),
213                        2 => (Vec3::new(u * half_extents.x, half_extents.y, v * half_extents.z), Vec3::Y),
214                        3 => (Vec3::new(u * half_extents.x, -half_extents.y, v * half_extents.z), Vec3::NEG_Y),
215                        4 => (Vec3::new(u * half_extents.x, v * half_extents.y, half_extents.z), Vec3::Z),
216                        _ => (Vec3::new(u * half_extents.x, v * half_extents.y, -half_extents.z), Vec3::NEG_Z),
217                    };
218                    (pos, normal)
219                } else {
220                    let p = Vec3::new(
221                        rng.next_f32_range(-half_extents.x, half_extents.x),
222                        rng.next_f32_range(-half_extents.y, half_extents.y),
223                        rng.next_f32_range(-half_extents.z, half_extents.z),
224                    );
225                    (p, Vec3::Y)
226                }
227            }
228            EmitterShape::Cone { radius, angle_deg, length } => {
229                let t = rng.next_f32();
230                let r = rng.next_f32().sqrt() * radius * t;
231                let angle = rng.next_f32() * std::f32::consts::TAU;
232                let x = angle.cos() * r;
233                let z = angle.sin() * r;
234                let y = t * length;
235                let spread = (angle_deg.to_radians()).tan();
236                let nx = x / length.max(0.001) * spread;
237                let nz = z / length.max(0.001) * spread;
238                let norm = Vec3::new(nx, 1.0, nz).normalize();
239                (Vec3::new(x, y, z), norm)
240            }
241            EmitterShape::Ring { radius, tube_radius } => {
242                let theta = rng.next_f32() * std::f32::consts::TAU;
243                let phi = rng.next_f32() * std::f32::consts::TAU;
244                let tr = rng.next_f32().sqrt() * tube_radius;
245                let cx = theta.cos() * radius;
246                let cz = theta.sin() * radius;
247                let tx = phi.cos() * tr;
248                let ty = phi.sin() * tr;
249                let pos = Vec3::new(cx + theta.cos() * tx, ty, cz + theta.sin() * tx);
250                let norm = Vec3::new(theta.cos() * phi.cos(), phi.sin(), theta.sin() * phi.cos());
251                (pos, norm.normalize())
252            }
253            EmitterShape::Disk { radius } => {
254                let r = rng.next_f32().sqrt() * radius;
255                let a = rng.next_f32() * std::f32::consts::TAU;
256                (Vec3::new(a.cos() * r, 0.0, a.sin() * r), Vec3::Y)
257            }
258            EmitterShape::Line { start, end } => {
259                let t = rng.next_f32();
260                let p = start.lerp(*end, t);
261                let dir = (*end - *start).normalize_or_zero();
262                (p, dir)
263            }
264            EmitterShape::Trail { points } => {
265                if points.is_empty() { return (Vec3::ZERO, Vec3::Y); }
266                let idx = (rng.next_f32() * (points.len() as f32)) as usize;
267                let idx = idx.min(points.len() - 1);
268                let next = (idx + 1).min(points.len() - 1);
269                let t = rng.next_f32();
270                let p = points[idx].lerp(points[next], t);
271                (p, Vec3::Y)
272            }
273            EmitterShape::Ribbon { points, width } => {
274                if points.is_empty() { return (Vec3::ZERO, Vec3::Y); }
275                let idx = (rng.next_f32() * (points.len() as f32)) as usize;
276                let idx = idx.min(points.len() - 1);
277                let u = rng.next_f32_range(-width * 0.5, width * 0.5);
278                (points[idx] + Vec3::new(u, 0.0, 0.0), Vec3::Y)
279            }
280            EmitterShape::Burst { radius, count } => {
281                let r = rng.next_f32().sqrt() * radius;
282                let a = rng.next_f32() * std::f32::consts::TAU;
283                (Vec3::new(a.cos() * r, 0.0, a.sin() * r), Vec3::Y)
284            }
285            EmitterShape::Vortex { radius, height, twist } => {
286                let t = rng.next_f32();
287                let y = t * height;
288                let angle = rng.next_f32() * std::f32::consts::TAU + t * twist;
289                let r = rng.next_f32().sqrt() * radius;
290                let pos = Vec3::new(angle.cos() * r, y, angle.sin() * r);
291                let tangent = Vec3::new(-angle.sin(), twist / height, angle.cos()).normalize();
292                (pos, tangent)
293            }
294            EmitterShape::Mesh { vertex_count, .. } => {
295                let idx = (rng.next_f32() * (*vertex_count as f32)) as u32;
296                let angle = (idx as f32 / *vertex_count as f32) * std::f32::consts::TAU;
297                (Vec3::new(angle.cos(), 0.0, angle.sin()), Vec3::Y)
298            }
299            EmitterShape::Skinned { bone_count } => {
300                let angle = rng.next_f32() * std::f32::consts::TAU;
301                let r = rng.next_f32();
302                (Vec3::new(angle.cos() * r, rng.next_f32(), angle.sin() * r), Vec3::Y)
303            }
304        }
305    }
306
307    pub fn name(&self) -> &'static str {
308        match self {
309            EmitterShape::Point => "Point",
310            EmitterShape::Sphere { .. } => "Sphere",
311            EmitterShape::Hemisphere { .. } => "Hemisphere",
312            EmitterShape::Box { .. } => "Box",
313            EmitterShape::Cone { .. } => "Cone",
314            EmitterShape::Ring { .. } => "Ring",
315            EmitterShape::Disk { .. } => "Disk",
316            EmitterShape::Line { .. } => "Line",
317            EmitterShape::Trail { .. } => "Trail",
318            EmitterShape::Ribbon { .. } => "Ribbon",
319            EmitterShape::Burst { .. } => "Burst",
320            EmitterShape::Vortex { .. } => "Vortex",
321            EmitterShape::Mesh { .. } => "Mesh",
322            EmitterShape::Skinned { .. } => "Skinned",
323        }
324    }
325}
326
327// ============================================================
328// PARTICLE MODULES
329// ============================================================
330
331#[derive(Clone, Debug)]
332pub struct LifetimeModule {
333    pub min_lifetime: f32,
334    pub max_lifetime: f32,
335    pub enabled: bool,
336}
337impl LifetimeModule {
338    pub fn new(min: f32, max: f32) -> Self { Self { min_lifetime: min, max_lifetime: max, enabled: true } }
339    pub fn sample(&self, rng: &mut SimpleRng) -> f32 {
340        rng.next_f32_range(self.min_lifetime, self.max_lifetime)
341    }
342}
343
344#[derive(Clone, Debug)]
345pub struct VelocityModule {
346    pub initial_speed_min: f32,
347    pub initial_speed_max: f32,
348    pub speed_over_lifetime: FloatCurve,
349    pub inherit_velocity: f32,
350    pub velocity_offset: Vec3,
351    pub orbital_velocity: Vec3,
352    pub radial_velocity: FloatCurve,
353    pub enabled: bool,
354}
355impl VelocityModule {
356    pub fn new(speed_min: f32, speed_max: f32) -> Self {
357        Self {
358            initial_speed_min: speed_min,
359            initial_speed_max: speed_max,
360            speed_over_lifetime: FloatCurve::constant(1.0),
361            inherit_velocity: 0.0,
362            velocity_offset: Vec3::ZERO,
363            orbital_velocity: Vec3::ZERO,
364            radial_velocity: FloatCurve::constant(0.0),
365            enabled: true,
366        }
367    }
368    pub fn sample_speed(&self, rng: &mut SimpleRng) -> f32 {
369        rng.next_f32_range(self.initial_speed_min, self.initial_speed_max)
370    }
371}
372
373#[derive(Clone, Debug)]
374pub struct ColorModule {
375    pub color_over_lifetime: ColorGradient,
376    pub color_by_speed: ColorGradient,
377    pub speed_range: Vec2,
378    pub start_color_min: Vec4,
379    pub start_color_max: Vec4,
380    pub enabled: bool,
381}
382impl ColorModule {
383    pub fn new() -> Self {
384        Self {
385            color_over_lifetime: ColorGradient::white(),
386            color_by_speed: ColorGradient::white(),
387            speed_range: Vec2::new(0.0, 10.0),
388            start_color_min: Vec4::ONE,
389            start_color_max: Vec4::ONE,
390            enabled: true,
391        }
392    }
393    pub fn sample_start_color(&self, rng: &mut SimpleRng) -> Vec4 {
394        let t = rng.next_f32();
395        self.start_color_min.lerp(self.start_color_max, t)
396    }
397}
398
399#[derive(Clone, Debug)]
400pub struct SizeModule {
401    pub start_size_min: f32,
402    pub start_size_max: f32,
403    pub size_over_lifetime: FloatCurve,
404    pub size_by_speed: FloatCurve,
405    pub speed_range: Vec2,
406    pub separate_axes: bool,
407    pub x_curve: FloatCurve,
408    pub y_curve: FloatCurve,
409    pub z_curve: FloatCurve,
410    pub enabled: bool,
411}
412impl SizeModule {
413    pub fn new(min: f32, max: f32) -> Self {
414        Self {
415            start_size_min: min,
416            start_size_max: max,
417            size_over_lifetime: FloatCurve::constant(1.0),
418            size_by_speed: FloatCurve::constant(1.0),
419            speed_range: Vec2::new(0.0, 10.0),
420            separate_axes: false,
421            x_curve: FloatCurve::constant(1.0),
422            y_curve: FloatCurve::constant(1.0),
423            z_curve: FloatCurve::constant(1.0),
424            enabled: true,
425        }
426    }
427    pub fn sample_start_size(&self, rng: &mut SimpleRng) -> f32 {
428        rng.next_f32_range(self.start_size_min, self.start_size_max)
429    }
430}
431
432#[derive(Clone, Debug)]
433pub struct RotationModule {
434    pub start_rotation_min: f32,
435    pub start_rotation_max: f32,
436    pub angular_velocity_min: f32,
437    pub angular_velocity_max: f32,
438    pub rotation_over_lifetime: FloatCurve,
439    pub rotation_by_speed: FloatCurve,
440    pub align_to_direction: bool,
441    pub enabled: bool,
442}
443impl RotationModule {
444    pub fn new() -> Self {
445        Self {
446            start_rotation_min: 0.0,
447            start_rotation_max: std::f32::consts::TAU,
448            angular_velocity_min: -1.0,
449            angular_velocity_max: 1.0,
450            rotation_over_lifetime: FloatCurve::constant(0.0),
451            rotation_by_speed: FloatCurve::constant(0.0),
452            align_to_direction: false,
453            enabled: true,
454        }
455    }
456    pub fn sample_start(&self, rng: &mut SimpleRng) -> f32 {
457        rng.next_f32_range(self.start_rotation_min, self.start_rotation_max)
458    }
459    pub fn sample_angular_velocity(&self, rng: &mut SimpleRng) -> f32 {
460        rng.next_f32_range(self.angular_velocity_min, self.angular_velocity_max)
461    }
462}
463
464#[derive(Clone, Debug)]
465pub struct GravityModule {
466    pub gravity_multiplier: f32,
467    pub gravity_direction: Vec3,
468    pub enabled: bool,
469}
470impl GravityModule {
471    pub fn new() -> Self {
472        Self { gravity_multiplier: 1.0, gravity_direction: Vec3::new(0.0, -9.81, 0.0), enabled: true }
473    }
474    pub fn force(&self) -> Vec3 { self.gravity_direction * self.gravity_multiplier }
475}
476
477#[derive(Clone, Debug)]
478pub struct NoiseModule {
479    pub frequency: f32,
480    pub amplitude: f32,
481    pub octaves: u32,
482    pub persistence: f32,
483    pub lacunarity: f32,
484    pub use_curl: bool,
485    pub scroll_speed: Vec3,
486    pub strength_over_lifetime: FloatCurve,
487    pub enabled: bool,
488}
489impl NoiseModule {
490    pub fn new() -> Self {
491        Self {
492            frequency: 1.0,
493            amplitude: 1.0,
494            octaves: 2,
495            persistence: 0.5,
496            lacunarity: 2.0,
497            use_curl: false,
498            scroll_speed: Vec3::ZERO,
499            strength_over_lifetime: FloatCurve::constant(1.0),
500            enabled: true,
501        }
502    }
503    pub fn evaluate(&self, pos: Vec3, time: f32) -> Vec3 {
504        let offset = self.scroll_speed * time;
505        let p = pos * self.frequency + offset;
506        if self.use_curl {
507            curl_noise(p, self.octaves, self.persistence, self.lacunarity) * self.amplitude
508        } else {
509            let nx = fbm_noise(p, self.octaves, self.persistence, self.lacunarity);
510            let ny = fbm_noise(p + Vec3::new(31.41, 17.83, 5.67), self.octaves, self.persistence, self.lacunarity);
511            let nz = fbm_noise(p + Vec3::new(7.13, 43.21, 23.99), self.octaves, self.persistence, self.lacunarity);
512            Vec3::new(nx, ny, nz) * self.amplitude
513        }
514    }
515}
516
517pub fn curl_noise(p: Vec3, octaves: u32, persistence: f32, lacunarity: f32) -> Vec3 {
518    let eps = 0.001_f32;
519    let ex = Vec3::new(eps, 0.0, 0.0);
520    let ey = Vec3::new(0.0, eps, 0.0);
521    let ez = Vec3::new(0.0, 0.0, eps);
522    let f = |q: Vec3| fbm_noise(q, octaves, persistence, lacunarity);
523    let dfdx = (f(p + ex) - f(p - ex)) / (2.0 * eps);
524    let dfdy = (f(p + ey) - f(p - ey)) / (2.0 * eps);
525    let dfdz = (f(p + ez) - f(p - ez)) / (2.0 * eps);
526    Vec3::new(dfdy - dfdz, dfdz - dfdx, dfdx - dfdy)
527}
528
529pub fn fbm_noise(p: Vec3, octaves: u32, persistence: f32, lacunarity: f32) -> f32 {
530    let mut result = 0.0_f32;
531    let mut amplitude = 1.0_f32;
532    let mut frequency = 1.0_f32;
533    let mut max_val = 0.0_f32;
534    for _ in 0..octaves {
535        result += perlin3(p * frequency) * amplitude;
536        max_val += amplitude;
537        amplitude *= persistence;
538        frequency *= lacunarity;
539    }
540    if max_val > 0.0 { result / max_val } else { 0.0 }
541}
542
543#[derive(Clone, Debug)]
544pub struct CollisionModule {
545    pub bounce: f32,
546    pub dampen: f32,
547    pub lifetime_loss: f32,
548    pub radius_scale: f32,
549    pub collision_quality: CollisionQuality,
550    pub planes: Vec<CollisionPlane>,
551    pub enabled: bool,
552}
553
554#[derive(Clone, Copy, Debug, PartialEq)]
555pub enum CollisionQuality { Low, Medium, High }
556
557#[derive(Clone, Debug)]
558pub struct CollisionPlane { pub normal: Vec3, pub distance: f32 }
559
560impl CollisionModule {
561    pub fn new() -> Self {
562        Self {
563            bounce: 0.3,
564            dampen: 0.1,
565            lifetime_loss: 0.0,
566            radius_scale: 1.0,
567            collision_quality: CollisionQuality::Medium,
568            planes: vec![CollisionPlane { normal: Vec3::Y, distance: 0.0 }],
569            enabled: true,
570        }
571    }
572    pub fn resolve(&self, pos: &mut Vec3, vel: &mut Vec3, radius: f32) {
573        for plane in &self.planes {
574            let dist = plane.normal.dot(*pos) - plane.distance;
575            if dist < radius * self.radius_scale {
576                let penetration = radius * self.radius_scale - dist;
577                *pos += plane.normal * penetration;
578                let vn = plane.normal.dot(*vel);
579                if vn < 0.0 {
580                    *vel -= plane.normal * vn * (1.0 + self.bounce);
581                    let vt = *vel - plane.normal * plane.normal.dot(*vel);
582                    *vel = plane.normal * plane.normal.dot(*vel) + vt * (1.0 - self.dampen);
583                }
584            }
585        }
586    }
587}
588
589#[derive(Clone, Debug)]
590pub struct TextureAnimationModule {
591    pub frame_count: u32,
592    pub fps: f32,
593    pub atlas_cols: u32,
594    pub atlas_rows: u32,
595    pub start_frame: u32,
596    pub random_start: bool,
597    pub loop_anim: bool,
598    pub enabled: bool,
599}
600impl TextureAnimationModule {
601    pub fn new(cols: u32, rows: u32, fps: f32) -> Self {
602        Self {
603            frame_count: cols * rows,
604            fps,
605            atlas_cols: cols,
606            atlas_rows: rows,
607            start_frame: 0,
608            random_start: false,
609            loop_anim: true,
610            enabled: true,
611        }
612    }
613    pub fn get_frame(&self, age: f32, lifetime: f32) -> u32 {
614        let total = self.frame_count.max(1);
615        let frame = (age * self.fps) as u32;
616        if self.loop_anim { frame % total } else { frame.min(total - 1) }
617    }
618    pub fn get_uv_offset(&self, frame: u32) -> Vec2 {
619        let col = frame % self.atlas_cols.max(1);
620        let row = frame / self.atlas_cols.max(1);
621        Vec2::new(
622            col as f32 / self.atlas_cols.max(1) as f32,
623            row as f32 / self.atlas_rows.max(1) as f32,
624        )
625    }
626    pub fn get_uv_scale(&self) -> Vec2 {
627        Vec2::new(
628            1.0 / self.atlas_cols.max(1) as f32,
629            1.0 / self.atlas_rows.max(1) as f32,
630        )
631    }
632}
633
634// ============================================================
635// FORCE FIELDS
636// ============================================================
637
638#[derive(Clone, Debug)]
639pub struct DirectionalForce {
640    pub direction: Vec3,
641    pub strength: f32,
642    pub randomness: f32,
643}
644impl DirectionalForce {
645    pub fn new(dir: Vec3, strength: f32) -> Self { Self { direction: dir.normalize_or_zero(), strength, randomness: 0.0 } }
646    pub fn apply(&self, vel: &Vec3, rng: &mut SimpleRng) -> Vec3 {
647        let noise = rng.next_unit_vec3() * self.randomness;
648        (self.direction + noise).normalize_or_zero() * self.strength
649    }
650}
651
652#[derive(Clone, Debug)]
653pub struct VortexForceField {
654    pub center: Vec3,
655    pub axis: Vec3,
656    pub strength: f32,
657    pub inward_strength: f32,
658    pub height: f32,
659}
660impl VortexForceField {
661    pub fn new(center: Vec3, strength: f32) -> Self {
662        Self { center, axis: Vec3::Y, strength, inward_strength: 0.0, height: 10.0 }
663    }
664    pub fn apply(&self, pos: Vec3) -> Vec3 {
665        let offset = pos - self.center;
666        let axial = self.axis * self.axis.dot(offset);
667        let radial = offset - axial;
668        if radial.length_squared() < 0.0001 { return Vec3::ZERO; }
669        let tangent = self.axis.cross(radial).normalize_or_zero();
670        tangent * self.strength + radial.normalize_or_zero() * (-self.inward_strength)
671    }
672}
673
674#[derive(Clone, Debug)]
675pub struct TurbulenceForce {
676    pub frequency: f32,
677    pub amplitude: f32,
678    pub octaves: u32,
679}
680impl TurbulenceForce {
681    pub fn new(freq: f32, amp: f32) -> Self { Self { frequency: freq, amplitude: amp, octaves: 2 } }
682    pub fn apply(&self, pos: Vec3, time: f32) -> Vec3 {
683        let p = pos * self.frequency + Vec3::splat(time * 0.1);
684        curl_noise(p, self.octaves, 0.5, 2.0) * self.amplitude
685    }
686}
687
688#[derive(Clone, Debug)]
689pub struct DragForce {
690    pub drag_coefficient: f32,
691    pub multiply_by_size: bool,
692}
693impl DragForce {
694    pub fn new(coeff: f32) -> Self { Self { drag_coefficient: coeff, multiply_by_size: false } }
695    pub fn apply(&self, vel: Vec3, size: f32) -> Vec3 {
696        let coeff = if self.multiply_by_size { self.drag_coefficient * size } else { self.drag_coefficient };
697        -vel * coeff
698    }
699}
700
701#[derive(Clone, Debug)]
702pub struct GravityPointForce {
703    pub center: Vec3,
704    pub strength: f32,
705    pub min_distance: f32,
706    pub max_distance: f32,
707    pub gravity_type: GravityType,
708}
709
710#[derive(Clone, Copy, Debug, PartialEq)]
711pub enum GravityType { Attract, Repel, Toggle }
712
713impl GravityPointForce {
714    pub fn new(center: Vec3, strength: f32) -> Self {
715        Self { center, strength, min_distance: 0.1, max_distance: 100.0, gravity_type: GravityType::Attract }
716    }
717    pub fn apply(&self, pos: Vec3) -> Vec3 {
718        let diff = self.center - pos;
719        let dist = diff.length();
720        if dist < self.min_distance || dist > self.max_distance { return Vec3::ZERO; }
721        let dir = diff / dist;
722        let force = self.strength / (dist * dist).max(0.01);
723        match self.gravity_type {
724            GravityType::Attract => dir * force,
725            GravityType::Repel => -dir * force,
726            GravityType::Toggle => if dist < (self.min_distance + self.max_distance) * 0.5 { -dir * force } else { dir * force },
727        }
728    }
729}
730
731#[derive(Clone, Debug)]
732pub struct WindForce {
733    pub base_direction: Vec3,
734    pub speed: f32,
735    pub turbulence: f32,
736    pub gust_frequency: f32,
737    pub gust_strength: f32,
738}
739impl WindForce {
740    pub fn new(dir: Vec3, speed: f32) -> Self {
741        Self { base_direction: dir.normalize_or_zero(), speed, turbulence: 0.1, gust_frequency: 0.5, gust_strength: 0.2 }
742    }
743    pub fn apply(&self, pos: Vec3, time: f32) -> Vec3 {
744        let gust = (time * self.gust_frequency * std::f32::consts::TAU).sin() * self.gust_strength;
745        let turb = fbm_noise(pos * 0.1 + Vec3::splat(time * 0.5), 2, 0.5, 2.0) * self.turbulence;
746        self.base_direction * (self.speed + gust + turb)
747    }
748}
749
750#[derive(Clone, Debug)]
751pub struct MagneticForce {
752    pub field_vector: Vec3,  // B field
753    pub charge: f32,         // q
754    pub enabled: bool,
755}
756impl MagneticForce {
757    pub fn new(b: Vec3, charge: f32) -> Self { Self { field_vector: b, charge, enabled: true } }
758    /// Lorentz force: F = q(v × B)
759    pub fn apply(&self, vel: Vec3) -> Vec3 {
760        self.charge * vel.cross(self.field_vector)
761    }
762}
763
764#[derive(Clone, Debug)]
765pub enum ForceFieldKindInner {
766    Directional(DirectionalForce),
767    Vortex(VortexForceField),
768    Turbulence(TurbulenceForce),
769    Drag(DragForce),
770    GravityPoint(GravityPointForce),
771    Wind(WindForce),
772    Magnetic(MagneticForce),
773}
774
775#[derive(Clone, Debug)]
776pub enum ForceFieldShape {
777    Global,
778    Sphere { radius: f32 },
779    Box { half_extents: Vec3 },
780    Cylinder { radius: f32, height: f32 },
781    Capsule { radius: f32, half_height: f32 },
782}
783
784impl ForceFieldShape {
785    pub fn contains(&self, pos: Vec3, center: Vec3) -> bool {
786        let offset = pos - center;
787        match self {
788            ForceFieldShape::Global => true,
789            ForceFieldShape::Sphere { radius } => offset.length_squared() <= radius * radius,
790            ForceFieldShape::Box { half_extents } => {
791                offset.x.abs() <= half_extents.x
792                    && offset.y.abs() <= half_extents.y
793                    && offset.z.abs() <= half_extents.z
794            }
795            ForceFieldShape::Cylinder { radius, height } => {
796                let r2 = offset.x * offset.x + offset.z * offset.z;
797                r2 <= radius * radius && offset.y.abs() <= height * 0.5
798            }
799            ForceFieldShape::Capsule { radius, half_height } => {
800                let clamped_y = offset.y.clamp(-*half_height, *half_height);
801                let closest = Vec3::new(0.0, clamped_y, 0.0);
802                (offset - closest).length_squared() <= radius * radius
803            }
804        }
805    }
806}
807
808#[derive(Clone, Debug)]
809pub struct ForceField {
810    pub name: String,
811    pub center: Vec3,
812    pub shape: ForceFieldShape,
813    pub kind: ForceFieldKindInner,
814    pub strength_multiplier: f32,
815    pub enabled: bool,
816    pub id: u64,
817}
818
819impl ForceField {
820    pub fn new(name: &str, kind: ForceFieldKindInner) -> Self {
821        Self {
822            name: name.to_string(),
823            center: Vec3::ZERO,
824            shape: ForceFieldShape::Global,
825            kind,
826            strength_multiplier: 1.0,
827            enabled: true,
828            id: 0,
829        }
830    }
831    pub fn apply(&self, pos: Vec3, vel: Vec3, size: f32, time: f32, rng: &mut SimpleRng) -> Vec3 {
832        if !self.enabled { return Vec3::ZERO; }
833        if !self.shape.contains(pos, self.center) { return Vec3::ZERO; }
834        let raw = match &self.kind {
835            ForceFieldKindInner::Directional(f) => f.apply(&vel, rng),
836            ForceFieldKindInner::Vortex(f) => f.apply(pos),
837            ForceFieldKindInner::Turbulence(f) => f.apply(pos, time),
838            ForceFieldKindInner::Drag(f) => f.apply(vel, size),
839            ForceFieldKindInner::GravityPoint(f) => f.apply(pos),
840            ForceFieldKindInner::Wind(f) => f.apply(pos, time),
841            ForceFieldKindInner::Magnetic(f) => f.apply(vel),
842        };
843        raw * self.strength_multiplier
844    }
845}
846
847// ============================================================
848// PERLIN NOISE
849// ============================================================
850
851static PERM: [u8; 512] = [
852    151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
853    140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
854    247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
855     57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
856     74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
857     60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
858     65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
859    200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
860     52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
861    207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
862    119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
863    129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
864    218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
865     81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
866    184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
867    222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
868    151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
869    140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
870    247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
871     57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
872     74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
873     60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
874     65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
875    200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
876     52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
877    207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
878    119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
879    129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
880    218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
881     81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
882    184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
883    222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
884];
885
886fn fade(t: f32) -> f32 { t * t * t * (t * (t * 6.0 - 15.0) + 10.0) }
887fn lerp_f(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) }
888fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
889    let h = hash & 15;
890    let u = if h < 8 { x } else { y };
891    let v = if h < 4 { y } else if h == 12 || h == 14 { x } else { z };
892    let sign_u = if h & 1 == 0 { u } else { -u };
893    let sign_v = if h & 2 == 0 { v } else { -v };
894    sign_u + sign_v
895}
896
897pub fn perlin3(p: Vec3) -> f32 {
898    let xi = p.x.floor() as i32 & 255;
899    let yi = p.y.floor() as i32 & 255;
900    let zi = p.z.floor() as i32 & 255;
901    let xf = p.x - p.x.floor();
902    let yf = p.y - p.y.floor();
903    let zf = p.z - p.z.floor();
904    let u = fade(xf); let v = fade(yf); let w = fade(zf);
905    let a  = PERM[xi as usize] as usize + yi as usize;
906    let aa = PERM[a  & 255] as usize + zi as usize;
907    let ab = PERM[(a+1) & 255] as usize + zi as usize;
908    let b  = PERM[(xi as usize + 1) & 255] as usize + yi as usize;
909    let ba = PERM[b  & 255] as usize + zi as usize;
910    let bb = PERM[(b+1) & 255] as usize + zi as usize;
911    lerp_f(
912        lerp_f(
913            lerp_f(grad(PERM[aa & 511], xf,     yf,     zf),     grad(PERM[ba & 511], xf-1.0, yf,     zf),     u),
914            lerp_f(grad(PERM[ab & 511], xf,     yf-1.0, zf),     grad(PERM[bb & 511], xf-1.0, yf-1.0, zf),     u),
915            v
916        ),
917        lerp_f(
918            lerp_f(grad(PERM[(aa+1) & 511], xf, yf,     zf-1.0), grad(PERM[(ba+1) & 511], xf-1.0, yf,     zf-1.0), u),
919            lerp_f(grad(PERM[(ab+1) & 511], xf, yf-1.0, zf-1.0), grad(PERM[(bb+1) & 511], xf-1.0, yf-1.0, zf-1.0), u),
920            v
921        ),
922        w
923    ) * 0.5 + 0.5
924}
925
926// ============================================================
927// SPATIAL HASH
928// ============================================================
929
930#[derive(Clone, Debug)]
931pub struct SpatialHash {
932    pub cell_size: f32,
933    pub cells: HashMap<(i32, i32, i32), Vec<usize>>,
934}
935
936impl SpatialHash {
937    pub fn new(cell_size: f32) -> Self {
938        Self { cell_size, cells: HashMap::new() }
939    }
940    pub fn clear(&mut self) { self.cells.clear(); }
941    fn cell(&self, pos: Vec3) -> (i32, i32, i32) {
942        (
943            (pos.x / self.cell_size).floor() as i32,
944            (pos.y / self.cell_size).floor() as i32,
945            (pos.z / self.cell_size).floor() as i32,
946        )
947    }
948    pub fn insert(&mut self, pos: Vec3, idx: usize) {
949        self.cells.entry(self.cell(pos)).or_default().push(idx);
950    }
951    pub fn query_radius(&self, pos: Vec3, radius: f32) -> Vec<usize> {
952        let cells = (radius / self.cell_size).ceil() as i32;
953        let c = self.cell(pos);
954        let mut result = Vec::new();
955        for dx in -cells..=cells {
956            for dy in -cells..=cells {
957                for dz in -cells..=cells {
958                    let key = (c.0 + dx, c.1 + dy, c.2 + dz);
959                    if let Some(indices) = self.cells.get(&key) {
960                        result.extend_from_slice(indices);
961                    }
962                }
963            }
964        }
965        result
966    }
967    pub fn count(&self) -> usize { self.cells.values().map(|v| v.len()).sum() }
968}
969
970// ============================================================
971// LOD SYSTEM
972// ============================================================
973
974#[derive(Clone, Copy, Debug, PartialEq)]
975pub enum SimQuality { Full, Half, Quarter, Minimal, Culled }
976
977#[derive(Clone, Debug)]
978pub struct LodLevel {
979    pub distance: f32,
980    pub quality: SimQuality,
981    pub emission_scale: f32,
982    pub simulation_rate: f32,
983}
984
985impl LodLevel {
986    pub fn new(distance: f32, quality: SimQuality, emission_scale: f32) -> Self {
987        let sim_rate = match quality {
988            SimQuality::Full => 1.0,
989            SimQuality::Half => 0.5,
990            SimQuality::Quarter => 0.25,
991            SimQuality::Minimal => 0.1,
992            SimQuality::Culled => 0.0,
993        };
994        Self { distance, quality, emission_scale, simulation_rate: sim_rate }
995    }
996}
997
998#[derive(Clone, Debug)]
999pub struct LodSystem {
1000    pub levels: Vec<LodLevel>,
1001    pub enabled: bool,
1002    pub fade_distance: f32,
1003}
1004
1005impl LodSystem {
1006    pub fn default_levels() -> Self {
1007        Self {
1008            levels: vec![
1009                LodLevel::new(10.0, SimQuality::Full, 1.0),
1010                LodLevel::new(30.0, SimQuality::Half, 0.75),
1011                LodLevel::new(60.0, SimQuality::Quarter, 0.5),
1012                LodLevel::new(100.0, SimQuality::Minimal, 0.25),
1013                LodLevel::new(f32::MAX, SimQuality::Culled, 0.0),
1014            ],
1015            enabled: true,
1016            fade_distance: 5.0,
1017        }
1018    }
1019    pub fn get_level(&self, distance: f32) -> &LodLevel {
1020        for level in &self.levels {
1021            if distance <= level.distance {
1022                return level;
1023            }
1024        }
1025        self.levels.last().unwrap_or(&self.levels[0])
1026    }
1027    pub fn get_emission_scale(&self, distance: f32) -> f32 {
1028        if !self.enabled { return 1.0; }
1029        self.get_level(distance).emission_scale
1030    }
1031}
1032
1033// ============================================================
1034// GPU PARAMS
1035// ============================================================
1036
1037#[derive(Clone, Debug)]
1038pub struct GpuDispatchSize {
1039    pub thread_group_x: u32,
1040    pub thread_group_y: u32,
1041    pub thread_group_z: u32,
1042}
1043
1044impl GpuDispatchSize {
1045    pub fn for_particles(count: u32, threads_per_group: u32) -> Self {
1046        let groups = (count + threads_per_group - 1) / threads_per_group;
1047        Self { thread_group_x: groups, thread_group_y: 1, thread_group_z: 1 }
1048    }
1049    pub fn total_threads(&self, threads_per_group: u32) -> u32 {
1050        self.thread_group_x * self.thread_group_y * self.thread_group_z * threads_per_group
1051    }
1052}
1053
1054#[derive(Clone, Debug)]
1055pub struct ParticleGpuBufferLayout {
1056    pub position_offset: u32,
1057    pub velocity_offset: u32,
1058    pub color_offset: u32,
1059    pub size_offset: u32,
1060    pub rotation_offset: u32,
1061    pub age_offset: u32,
1062    pub lifetime_offset: u32,
1063    pub custom0_offset: u32,
1064    pub stride: u32,
1065}
1066
1067impl ParticleGpuBufferLayout {
1068    pub fn packed() -> Self {
1069        Self {
1070            position_offset: 0,
1071            velocity_offset: 12,
1072            color_offset: 24,
1073            size_offset: 40,
1074            rotation_offset: 44,
1075            age_offset: 48,
1076            lifetime_offset: 52,
1077            custom0_offset: 56,
1078            stride: 64,
1079        }
1080    }
1081    pub fn buffer_size(&self, count: u32) -> u64 {
1082        self.stride as u64 * count as u64
1083    }
1084}
1085
1086#[derive(Clone, Debug)]
1087pub struct GpuParticleParams {
1088    pub max_particles: u32,
1089    pub threads_per_group: u32,
1090    pub buffer_layout: ParticleGpuBufferLayout,
1091    pub use_compute_simulate: bool,
1092    pub use_indirect_draw: bool,
1093    pub use_gpu_sort: bool,
1094    pub sort_key_bits: u32,
1095}
1096
1097impl GpuParticleParams {
1098    pub fn default_params(max_particles: u32) -> Self {
1099        Self {
1100            max_particles,
1101            threads_per_group: 256,
1102            buffer_layout: ParticleGpuBufferLayout::packed(),
1103            use_compute_simulate: true,
1104            use_indirect_draw: true,
1105            use_gpu_sort: false,
1106            sort_key_bits: 32,
1107        }
1108    }
1109    pub fn dispatch_size(&self) -> GpuDispatchSize {
1110        GpuDispatchSize::for_particles(self.max_particles, self.threads_per_group)
1111    }
1112    pub fn buffer_bytes(&self) -> u64 {
1113        self.buffer_layout.buffer_size(self.max_particles)
1114    }
1115}
1116
1117// ============================================================
1118// PARTICLE
1119// ============================================================
1120
1121#[derive(Clone, Debug)]
1122pub struct Particle {
1123    pub position: Vec3,
1124    pub prev_position: Vec3,   // Verlet
1125    pub velocity: Vec3,
1126    pub acceleration: Vec3,
1127    pub color: Vec4,
1128    pub size: f32,
1129    pub rotation: f32,
1130    pub angular_velocity: f32,
1131    pub age: f32,
1132    pub lifetime: f32,
1133    pub frame: u32,
1134    pub custom: Vec4,
1135    pub alive: bool,
1136}
1137
1138impl Particle {
1139    pub fn new(pos: Vec3, vel: Vec3, lifetime: f32, color: Vec4, size: f32) -> Self {
1140        Self {
1141            position: pos,
1142            prev_position: pos,
1143            velocity: vel,
1144            acceleration: Vec3::ZERO,
1145            color,
1146            size,
1147            rotation: 0.0,
1148            angular_velocity: 0.0,
1149            age: 0.0,
1150            lifetime,
1151            frame: 0,
1152            custom: Vec4::ZERO,
1153            alive: true,
1154        }
1155    }
1156    pub fn normalized_age(&self) -> f32 {
1157        if self.lifetime <= 0.0 { 1.0 } else { (self.age / self.lifetime).clamp(0.0, 1.0) }
1158    }
1159    /// Verlet integration
1160    pub fn integrate_verlet(&mut self, dt: f32) {
1161        let new_pos = self.position * 2.0 - self.prev_position + self.acceleration * dt * dt;
1162        self.prev_position = self.position;
1163        self.velocity = (new_pos - self.position) / dt.max(0.00001);
1164        self.position = new_pos;
1165        self.acceleration = Vec3::ZERO;
1166    }
1167    pub fn integrate_euler(&mut self, dt: f32) {
1168        self.velocity += self.acceleration * dt;
1169        self.position += self.velocity * dt;
1170        self.acceleration = Vec3::ZERO;
1171    }
1172    pub fn apply_force(&mut self, force: Vec3) {
1173        self.acceleration += force;
1174    }
1175    pub fn update_rotation(&mut self, dt: f32) {
1176        self.rotation += self.angular_velocity * dt;
1177    }
1178    pub fn is_dead(&self) -> bool { !self.alive || self.age >= self.lifetime }
1179}
1180
1181// ============================================================
1182// RENDER SETTINGS
1183// ============================================================
1184
1185#[derive(Clone, Copy, Debug, PartialEq)]
1186pub enum ParticleRenderMode {
1187    Billboard,
1188    StretchedBillboard,
1189    HorizontalBillboard,
1190    VerticalBillboard,
1191    Mesh,
1192    None,
1193}
1194
1195#[derive(Clone, Copy, Debug, PartialEq)]
1196pub enum ParticleBlendMode {
1197    Alpha,
1198    Additive,
1199    Multiply,
1200    Premultiplied,
1201    Subtractive,
1202}
1203
1204#[derive(Clone, Copy, Debug, PartialEq)]
1205pub enum SortMode {
1206    None,
1207    ByDistance,
1208    YoungestFirst,
1209    OldestFirst,
1210}
1211
1212// ============================================================
1213// PARTICLE EMITTER
1214// ============================================================
1215
1216#[derive(Clone, Debug)]
1217pub struct EmissionBurst {
1218    pub time: f32,
1219    pub count_min: u32,
1220    pub count_max: u32,
1221    pub cycles: u32,
1222    pub interval: f32,
1223    pub probability: f32,
1224    pub fired_cycles: u32,
1225    pub next_time: f32,
1226}
1227
1228impl EmissionBurst {
1229    pub fn new(time: f32, count: u32) -> Self {
1230        Self {
1231            time,
1232            count_min: count,
1233            count_max: count,
1234            cycles: 1,
1235            interval: 0.1,
1236            probability: 1.0,
1237            fired_cycles: 0,
1238            next_time: time,
1239        }
1240    }
1241}
1242
1243#[derive(Clone, Debug)]
1244pub struct ParticleEmitter {
1245    pub name: String,
1246    pub id: u64,
1247    pub enabled: bool,
1248    pub position: Vec3,
1249    pub rotation: Quat,
1250    pub shape: EmitterShape,
1251
1252    // Emission
1253    pub emission_rate: f32,
1254    pub emission_bursts: Vec<EmissionBurst>,
1255    pub max_particles: u32,
1256    pub duration: f32,
1257    pub looping: bool,
1258    pub prewarm: bool,
1259    pub start_delay: f32,
1260
1261    // Modules
1262    pub lifetime_module: LifetimeModule,
1263    pub velocity_module: VelocityModule,
1264    pub color_module: ColorModule,
1265    pub size_module: SizeModule,
1266    pub rotation_module: RotationModule,
1267    pub gravity_module: GravityModule,
1268    pub noise_module: NoiseModule,
1269    pub collision_module: CollisionModule,
1270    pub texture_anim_module: TextureAnimationModule,
1271
1272    // Render
1273    pub render_mode: ParticleRenderMode,
1274    pub blend_mode: ParticleBlendMode,
1275    pub sort_mode: SortMode,
1276    pub texture_id: u64,
1277    pub material_id: u64,
1278    pub render_order: i32,
1279    pub cast_shadows: bool,
1280    pub receive_shadows: bool,
1281    pub stretch_speed: f32,
1282
1283    // Internal state
1284    pub particles: Vec<Particle>,
1285    pub rng: SimpleRng,
1286    pub time: f32,
1287    pub elapsed: f32,
1288    pub emission_accumulator: f32,
1289    pub is_playing: bool,
1290    pub is_stopped: bool,
1291    pub spatial_hash: SpatialHash,
1292    pub lod: LodSystem,
1293    pub gpu_params: GpuParticleParams,
1294    pub statistics: EmitterStatistics,
1295}
1296
1297#[derive(Clone, Debug, Default)]
1298pub struct EmitterStatistics {
1299    pub alive_count: u32,
1300    pub total_spawned: u64,
1301    pub update_time_us: u64,
1302    pub spawn_time_us: u64,
1303    pub peak_count: u32,
1304}
1305
1306impl ParticleEmitter {
1307    pub fn new(name: &str) -> Self {
1308        Self {
1309            name: name.to_string(),
1310            id: 0,
1311            enabled: true,
1312            position: Vec3::ZERO,
1313            rotation: Quat::IDENTITY,
1314            shape: EmitterShape::Point,
1315            emission_rate: 10.0,
1316            emission_bursts: Vec::new(),
1317            max_particles: 1000,
1318            duration: 5.0,
1319            looping: true,
1320            prewarm: false,
1321            start_delay: 0.0,
1322            lifetime_module: LifetimeModule::new(1.0, 3.0),
1323            velocity_module: VelocityModule::new(1.0, 5.0),
1324            color_module: ColorModule::new(),
1325            size_module: SizeModule::new(0.1, 0.5),
1326            rotation_module: RotationModule::new(),
1327            gravity_module: GravityModule::new(),
1328            noise_module: NoiseModule::new(),
1329            collision_module: CollisionModule::new(),
1330            texture_anim_module: TextureAnimationModule::new(1, 1, 12.0),
1331            render_mode: ParticleRenderMode::Billboard,
1332            blend_mode: ParticleBlendMode::Alpha,
1333            sort_mode: SortMode::None,
1334            texture_id: 0,
1335            material_id: 0,
1336            render_order: 0,
1337            cast_shadows: false,
1338            receive_shadows: false,
1339            stretch_speed: 1.0,
1340            particles: Vec::new(),
1341            rng: SimpleRng::new(12345),
1342            time: 0.0,
1343            elapsed: 0.0,
1344            emission_accumulator: 0.0,
1345            is_playing: false,
1346            is_stopped: true,
1347            spatial_hash: SpatialHash::new(1.0),
1348            lod: LodSystem::default_levels(),
1349            gpu_params: GpuParticleParams::default_params(1000),
1350            statistics: EmitterStatistics::default(),
1351        }
1352    }
1353
1354    pub fn play(&mut self) { self.is_playing = true; self.is_stopped = false; }
1355    pub fn stop(&mut self) { self.is_playing = false; self.is_stopped = true; }
1356    pub fn pause(&mut self) { self.is_playing = false; }
1357    pub fn reset(&mut self) {
1358        self.time = 0.0;
1359        self.elapsed = 0.0;
1360        self.emission_accumulator = 0.0;
1361        self.particles.clear();
1362        self.statistics = EmitterStatistics::default();
1363    }
1364
1365    pub fn spawn_particle(&mut self) {
1366        if self.particles.len() >= self.max_particles as usize { return; }
1367        let (local_pos, normal) = self.shape.sample_position(&mut self.rng);
1368        let world_pos = self.position + self.rotation * local_pos;
1369        let speed = self.velocity_module.sample_speed(&mut self.rng);
1370        let dir = self.rotation * normal;
1371        let vel = dir * speed + self.velocity_module.velocity_offset;
1372        let lifetime = self.lifetime_module.sample(&mut self.rng);
1373        let color = self.color_module.sample_start_color(&mut self.rng);
1374        let size = self.size_module.sample_start_size(&mut self.rng);
1375        let mut p = Particle::new(world_pos, vel, lifetime, color, size);
1376        p.rotation = self.rotation_module.sample_start(&mut self.rng);
1377        p.angular_velocity = self.rotation_module.sample_angular_velocity(&mut self.rng);
1378        self.particles.push(p);
1379        self.statistics.total_spawned += 1;
1380    }
1381
1382    pub fn update(&mut self, dt: f32, force_fields: &[ForceField]) {
1383        if !self.is_playing { return; }
1384        self.time += dt;
1385        self.elapsed += dt;
1386
1387        // Emission
1388        if self.emission_rate > 0.0 {
1389            self.emission_accumulator += self.emission_rate * dt;
1390            while self.emission_accumulator >= 1.0 {
1391                self.spawn_particle();
1392                self.emission_accumulator -= 1.0;
1393            }
1394        }
1395
1396        // Bursts
1397        let mut burst_spawn_count = 0u32;
1398        for burst in &mut self.emission_bursts {
1399            if self.elapsed >= burst.next_time && burst.fired_cycles < burst.cycles.max(1) {
1400                if self.rng.next_f32() <= burst.probability {
1401                    let count = if burst.count_min == burst.count_max {
1402                        burst.count_min
1403                    } else {
1404                        self.rng.next_u64() as u32 % (burst.count_max - burst.count_min + 1) + burst.count_min
1405                    };
1406                    burst_spawn_count += count;
1407                }
1408                burst.fired_cycles += 1;
1409                burst.next_time += burst.interval;
1410            }
1411        }
1412        for _ in 0..burst_spawn_count { self.spawn_particle(); }
1413
1414        // Update particles
1415        let gravity = self.gravity_module.force();
1416        let noise_mod = &self.noise_module;
1417        let col_mod = &self.collision_module;
1418        let tex_mod = &self.texture_anim_module;
1419        let color_mod = &self.color_module;
1420        let size_mod = &self.size_module;
1421
1422        for p in &mut self.particles {
1423            if !p.alive { continue; }
1424            p.age += dt;
1425            if p.is_dead() { p.alive = false; continue; }
1426            let t = p.normalized_age();
1427
1428            // Gravity
1429            if self.gravity_module.enabled {
1430                p.apply_force(gravity);
1431            }
1432
1433            // Noise
1434            if noise_mod.enabled {
1435                let strength = noise_mod.strength_over_lifetime.evaluate(t);
1436                let n = noise_mod.evaluate(p.position, self.elapsed);
1437                p.apply_force(n * strength);
1438            }
1439
1440            // Force fields
1441            for ff in force_fields {
1442                let f = ff.apply(p.position, p.velocity, p.size, self.elapsed, &mut self.rng);
1443                p.apply_force(f);
1444            }
1445
1446            // Integration
1447            p.integrate_verlet(dt);
1448
1449            // Collision
1450            if col_mod.enabled {
1451                col_mod.resolve(&mut p.position, &mut p.velocity, p.size);
1452            }
1453
1454            // Rotation
1455            p.update_rotation(dt);
1456
1457            // Color over lifetime
1458            if color_mod.enabled {
1459                p.color = color_mod.color_over_lifetime.evaluate(t);
1460            }
1461
1462            // Size over lifetime
1463            if size_mod.enabled {
1464                p.size *= size_mod.size_over_lifetime.evaluate(t);
1465            }
1466
1467            // Texture anim
1468            if tex_mod.enabled {
1469                p.frame = tex_mod.get_frame(p.age, p.lifetime);
1470            }
1471        }
1472
1473        // Compact dead particles
1474        self.particles.retain(|p| p.alive);
1475
1476        // Update stats
1477        self.statistics.alive_count = self.particles.len() as u32;
1478        if self.statistics.alive_count > self.statistics.peak_count {
1479            self.statistics.peak_count = self.statistics.alive_count;
1480        }
1481
1482        // Rebuild spatial hash
1483        self.spatial_hash.clear();
1484        for (i, p) in self.particles.iter().enumerate() {
1485            self.spatial_hash.insert(p.position, i);
1486        }
1487    }
1488}
1489
1490// ============================================================
1491// PARTICLE SYSTEM
1492// ============================================================
1493
1494#[derive(Clone, Debug)]
1495pub struct ParticleSystem {
1496    pub name: String,
1497    pub id: u64,
1498    pub emitters: Vec<ParticleEmitter>,
1499    pub force_fields: Vec<ForceField>,
1500    pub world_position: Vec3,
1501    pub world_rotation: Quat,
1502    pub time_scale: f32,
1503    pub enabled: bool,
1504    pub lod: LodSystem,
1505}
1506
1507impl ParticleSystem {
1508    pub fn new(name: &str) -> Self {
1509        Self {
1510            name: name.to_string(),
1511            id: 0,
1512            emitters: Vec::new(),
1513            force_fields: Vec::new(),
1514            world_position: Vec3::ZERO,
1515            world_rotation: Quat::IDENTITY,
1516            time_scale: 1.0,
1517            enabled: true,
1518            lod: LodSystem::default_levels(),
1519        }
1520    }
1521    pub fn add_emitter(&mut self, mut e: ParticleEmitter) -> u64 {
1522        let id = self.emitters.len() as u64 + 1;
1523        e.id = id;
1524        self.emitters.push(e);
1525        id
1526    }
1527    pub fn add_force_field(&mut self, mut ff: ForceField) -> u64 {
1528        let id = self.force_fields.len() as u64 + 1;
1529        ff.id = id;
1530        self.force_fields.push(ff);
1531        id
1532    }
1533    pub fn play_all(&mut self) { for e in &mut self.emitters { e.play(); } }
1534    pub fn stop_all(&mut self) { for e in &mut self.emitters { e.stop(); } }
1535    pub fn reset_all(&mut self) { for e in &mut self.emitters { e.reset(); } }
1536    pub fn update(&mut self, dt: f32) {
1537        if !self.enabled { return; }
1538        let scaled_dt = dt * self.time_scale;
1539        let ffs = self.force_fields.clone();
1540        for e in &mut self.emitters {
1541            e.update(scaled_dt, &ffs);
1542        }
1543    }
1544    pub fn total_alive(&self) -> u32 {
1545        self.emitters.iter().map(|e| e.statistics.alive_count).sum()
1546    }
1547    pub fn total_spawned(&self) -> u64 {
1548        self.emitters.iter().map(|e| e.statistics.total_spawned).sum()
1549    }
1550}
1551
1552// ============================================================
1553// PRESETS
1554// ============================================================
1555
1556pub fn preset_fire() -> ParticleSystem {
1557    let mut sys = ParticleSystem::new("Fire");
1558    let mut e = ParticleEmitter::new("Fire Emitter");
1559    e.shape = EmitterShape::Disk { radius: 0.5 };
1560    e.emission_rate = 80.0;
1561    e.max_particles = 500;
1562    e.lifetime_module = LifetimeModule::new(0.5, 1.5);
1563    e.velocity_module = VelocityModule::new(1.0, 3.0);
1564    e.velocity_module.velocity_offset = Vec3::new(0.0, 2.0, 0.0);
1565    e.size_module = SizeModule::new(0.2, 0.8);
1566    e.blend_mode = ParticleBlendMode::Additive;
1567    e.gravity_module.gravity_multiplier = -0.3;
1568    e.noise_module.enabled = true;
1569    e.noise_module.amplitude = 0.5;
1570    let mut col = ColorGradient::new();
1571    col.add_key(GradientKey::new(0.0, Vec4::new(1.0, 0.8, 0.0, 1.0)));
1572    col.add_key(GradientKey::new(0.5, Vec4::new(1.0, 0.3, 0.0, 0.8)));
1573    col.add_key(GradientKey::new(1.0, Vec4::new(0.2, 0.0, 0.0, 0.0)));
1574    e.color_module.color_over_lifetime = col;
1575    sys.add_emitter(e);
1576    sys
1577}
1578
1579pub fn preset_smoke() -> ParticleSystem {
1580    let mut sys = ParticleSystem::new("Smoke");
1581    let mut e = ParticleEmitter::new("Smoke Emitter");
1582    e.shape = EmitterShape::Disk { radius: 0.3 };
1583    e.emission_rate = 15.0;
1584    e.max_particles = 200;
1585    e.lifetime_module = LifetimeModule::new(3.0, 6.0);
1586    e.velocity_module = VelocityModule::new(0.2, 0.8);
1587    e.velocity_module.velocity_offset = Vec3::new(0.0, 1.0, 0.0);
1588    e.size_module = SizeModule::new(0.5, 2.0);
1589    e.blend_mode = ParticleBlendMode::Alpha;
1590    e.noise_module.enabled = true;
1591    e.noise_module.amplitude = 0.2;
1592    let mut col = ColorGradient::new();
1593    col.add_key(GradientKey::new(0.0, Vec4::new(0.6, 0.6, 0.6, 0.8)));
1594    col.add_key(GradientKey::new(1.0, Vec4::new(0.8, 0.8, 0.8, 0.0)));
1595    e.color_module.color_over_lifetime = col;
1596    sys.add_emitter(e);
1597    sys
1598}
1599
1600pub fn preset_explosion() -> ParticleSystem {
1601    let mut sys = ParticleSystem::new("Explosion");
1602    let mut e = ParticleEmitter::new("Explosion Emitter");
1603    e.shape = EmitterShape::Sphere { radius: 0.3, emit_from_shell: true };
1604    e.emission_rate = 0.0;
1605    e.emission_bursts = vec![EmissionBurst::new(0.0, 200)];
1606    e.max_particles = 500;
1607    e.lifetime_module = LifetimeModule::new(0.5, 2.0);
1608    e.velocity_module = VelocityModule::new(5.0, 20.0);
1609    e.size_module = SizeModule::new(0.1, 0.5);
1610    e.blend_mode = ParticleBlendMode::Additive;
1611    e.gravity_module.gravity_multiplier = 0.5;
1612    let mut col = ColorGradient::new();
1613    col.add_key(GradientKey::new(0.0, Vec4::new(1.0, 1.0, 0.5, 1.0)));
1614    col.add_key(GradientKey::new(0.3, Vec4::new(1.0, 0.4, 0.0, 1.0)));
1615    col.add_key(GradientKey::new(1.0, Vec4::new(0.1, 0.1, 0.1, 0.0)));
1616    e.color_module.color_over_lifetime = col;
1617    sys.add_emitter(e);
1618    sys
1619}
1620
1621pub fn preset_rain() -> ParticleSystem {
1622    let mut sys = ParticleSystem::new("Rain");
1623    let mut e = ParticleEmitter::new("Rain Emitter");
1624    e.shape = EmitterShape::Box { half_extents: Vec3::new(10.0, 0.0, 10.0), emit_from_shell: false };
1625    e.position = Vec3::new(0.0, 20.0, 0.0);
1626    e.emission_rate = 500.0;
1627    e.max_particles = 3000;
1628    e.lifetime_module = LifetimeModule::new(1.5, 2.5);
1629    e.velocity_module = VelocityModule::new(0.0, 0.0);
1630    e.velocity_module.velocity_offset = Vec3::new(0.0, -12.0, 0.0);
1631    e.size_module = SizeModule::new(0.02, 0.05);
1632    e.blend_mode = ParticleBlendMode::Alpha;
1633    e.render_mode = ParticleRenderMode::StretchedBillboard;
1634    e.stretch_speed = 0.5;
1635    let mut col = ColorGradient::new();
1636    col.add_key(GradientKey::new(0.0, Vec4::new(0.6, 0.8, 1.0, 0.7)));
1637    col.add_key(GradientKey::new(1.0, Vec4::new(0.6, 0.8, 1.0, 0.0)));
1638    e.color_module.color_over_lifetime = col;
1639    e.collision_module.enabled = true;
1640    sys.add_emitter(e);
1641    sys
1642}
1643
1644pub fn preset_sparks() -> ParticleSystem {
1645    let mut sys = ParticleSystem::new("Sparks");
1646    let mut e = ParticleEmitter::new("Sparks Emitter");
1647    e.shape = EmitterShape::Point;
1648    e.emission_rate = 0.0;
1649    e.emission_bursts = vec![EmissionBurst::new(0.0, 100)];
1650    e.max_particles = 200;
1651    e.lifetime_module = LifetimeModule::new(0.5, 2.0);
1652    e.velocity_module = VelocityModule::new(3.0, 10.0);
1653    e.size_module = SizeModule::new(0.02, 0.08);
1654    e.blend_mode = ParticleBlendMode::Additive;
1655    e.gravity_module.gravity_multiplier = 1.0;
1656    e.render_mode = ParticleRenderMode::StretchedBillboard;
1657    let mut col = ColorGradient::new();
1658    col.add_key(GradientKey::new(0.0, Vec4::new(1.0, 1.0, 0.5, 1.0)));
1659    col.add_key(GradientKey::new(0.5, Vec4::new(1.0, 0.5, 0.0, 0.8)));
1660    col.add_key(GradientKey::new(1.0, Vec4::new(0.5, 0.2, 0.0, 0.0)));
1661    e.color_module.color_over_lifetime = col;
1662    sys.add_emitter(e);
1663    sys
1664}
1665
1666pub fn preset_magic_trail() -> ParticleSystem {
1667    let mut sys = ParticleSystem::new("Magic Trail");
1668    let mut e = ParticleEmitter::new("Magic Emitter");
1669    e.shape = EmitterShape::Point;
1670    e.emission_rate = 50.0;
1671    e.max_particles = 300;
1672    e.lifetime_module = LifetimeModule::new(0.5, 1.5);
1673    e.velocity_module = VelocityModule::new(0.1, 0.5);
1674    e.size_module = SizeModule::new(0.05, 0.2);
1675    e.blend_mode = ParticleBlendMode::Additive;
1676    e.noise_module.enabled = true;
1677    e.noise_module.amplitude = 0.5;
1678    e.noise_module.frequency = 2.0;
1679    let mut col = ColorGradient::new();
1680    col.add_key(GradientKey::new(0.0, Vec4::new(0.5, 0.0, 1.0, 1.0)));
1681    col.add_key(GradientKey::new(0.5, Vec4::new(0.0, 0.5, 1.0, 0.8)));
1682    col.add_key(GradientKey::new(1.0, Vec4::new(0.0, 0.0, 0.5, 0.0)));
1683    e.color_module.color_over_lifetime = col;
1684    sys.add_emitter(e);
1685    sys
1686}
1687
1688pub fn preset_snow() -> ParticleSystem {
1689    let mut sys = ParticleSystem::new("Snow");
1690    let mut e = ParticleEmitter::new("Snow Emitter");
1691    e.shape = EmitterShape::Box { half_extents: Vec3::new(15.0, 0.0, 15.0), emit_from_shell: false };
1692    e.position = Vec3::new(0.0, 15.0, 0.0);
1693    e.emission_rate = 100.0;
1694    e.max_particles = 1000;
1695    e.lifetime_module = LifetimeModule::new(5.0, 10.0);
1696    e.velocity_module = VelocityModule::new(0.0, 0.0);
1697    e.velocity_module.velocity_offset = Vec3::new(0.0, -1.5, 0.0);
1698    e.size_module = SizeModule::new(0.05, 0.15);
1699    e.blend_mode = ParticleBlendMode::Alpha;
1700    e.noise_module.enabled = true;
1701    e.noise_module.amplitude = 0.3;
1702    e.noise_module.frequency = 0.5;
1703    let mut col = ColorGradient::new();
1704    col.add_key(GradientKey::new(0.0, Vec4::new(1.0, 1.0, 1.0, 0.9)));
1705    col.add_key(GradientKey::new(1.0, Vec4::new(1.0, 1.0, 1.0, 0.0)));
1706    e.color_module.color_over_lifetime = col;
1707    sys.add_emitter(e);
1708    sys
1709}
1710
1711pub fn preset_dust() -> ParticleSystem {
1712    let mut sys = ParticleSystem::new("Dust");
1713    let mut e = ParticleEmitter::new("Dust Emitter");
1714    e.shape = EmitterShape::Sphere { radius: 0.5, emit_from_shell: false };
1715    e.emission_rate = 20.0;
1716    e.max_particles = 200;
1717    e.lifetime_module = LifetimeModule::new(1.0, 3.0);
1718    e.velocity_module = VelocityModule::new(0.1, 0.5);
1719    e.size_module = SizeModule::new(0.1, 0.4);
1720    e.blend_mode = ParticleBlendMode::Alpha;
1721    e.gravity_module.gravity_multiplier = -0.05;
1722    e.noise_module.enabled = true;
1723    e.noise_module.amplitude = 0.1;
1724    let mut col = ColorGradient::new();
1725    col.add_key(GradientKey::new(0.0, Vec4::new(0.8, 0.7, 0.5, 0.5)));
1726    col.add_key(GradientKey::new(1.0, Vec4::new(0.8, 0.7, 0.5, 0.0)));
1727    e.color_module.color_over_lifetime = col;
1728    sys.add_emitter(e);
1729    sys
1730}
1731
1732pub fn preset_bubbles() -> ParticleSystem {
1733    let mut sys = ParticleSystem::new("Bubbles");
1734    let mut e = ParticleEmitter::new("Bubble Emitter");
1735    e.shape = EmitterShape::Disk { radius: 1.0 };
1736    e.emission_rate = 10.0;
1737    e.max_particles = 100;
1738    e.lifetime_module = LifetimeModule::new(3.0, 6.0);
1739    e.velocity_module = VelocityModule::new(0.2, 0.8);
1740    e.velocity_module.velocity_offset = Vec3::new(0.0, 1.0, 0.0);
1741    e.size_module = SizeModule::new(0.1, 0.4);
1742    e.blend_mode = ParticleBlendMode::Alpha;
1743    e.gravity_module.gravity_multiplier = -0.2;
1744    e.noise_module.enabled = true;
1745    e.noise_module.amplitude = 0.2;
1746    let mut col = ColorGradient::new();
1747    col.add_key(GradientKey::new(0.0, Vec4::new(0.4, 0.7, 1.0, 0.6)));
1748    col.add_key(GradientKey::new(0.8, Vec4::new(0.6, 0.9, 1.0, 0.4)));
1749    col.add_key(GradientKey::new(1.0, Vec4::new(1.0, 1.0, 1.0, 0.0)));
1750    e.color_module.color_over_lifetime = col;
1751    sys.add_emitter(e);
1752    sys
1753}
1754
1755pub fn preset_electricity() -> ParticleSystem {
1756    let mut sys = ParticleSystem::new("Electricity");
1757    let mut e = ParticleEmitter::new("Arc Emitter");
1758    e.shape = EmitterShape::Line { start: Vec3::ZERO, end: Vec3::new(0.0, 3.0, 0.0) };
1759    e.emission_rate = 100.0;
1760    e.max_particles = 300;
1761    e.lifetime_module = LifetimeModule::new(0.05, 0.2);
1762    e.velocity_module = VelocityModule::new(0.0, 0.5);
1763    e.size_module = SizeModule::new(0.02, 0.06);
1764    e.blend_mode = ParticleBlendMode::Additive;
1765    e.noise_module.enabled = true;
1766    e.noise_module.amplitude = 1.0;
1767    e.noise_module.frequency = 5.0;
1768    let mut col = ColorGradient::new();
1769    col.add_key(GradientKey::new(0.0, Vec4::new(0.7, 0.9, 1.0, 1.0)));
1770    col.add_key(GradientKey::new(0.5, Vec4::new(0.4, 0.6, 1.0, 0.8)));
1771    col.add_key(GradientKey::new(1.0, Vec4::new(0.2, 0.2, 1.0, 0.0)));
1772    e.color_module.color_over_lifetime = col;
1773    sys.add_emitter(e);
1774    sys
1775}
1776
1777pub fn preset_leaves() -> ParticleSystem {
1778    let mut sys = ParticleSystem::new("Falling Leaves");
1779    let mut e = ParticleEmitter::new("Leaf Emitter");
1780    e.shape = EmitterShape::Box { half_extents: Vec3::new(8.0, 0.0, 8.0), emit_from_shell: false };
1781    e.position = Vec3::new(0.0, 12.0, 0.0);
1782    e.emission_rate = 5.0;
1783    e.max_particles = 100;
1784    e.lifetime_module = LifetimeModule::new(5.0, 10.0);
1785    e.velocity_module = VelocityModule::new(0.0, 0.5);
1786    e.velocity_module.velocity_offset = Vec3::new(0.5, -2.0, 0.0);
1787    e.size_module = SizeModule::new(0.1, 0.3);
1788    e.rotation_module = RotationModule::new();
1789    e.rotation_module.angular_velocity_min = -2.0;
1790    e.rotation_module.angular_velocity_max = 2.0;
1791    e.blend_mode = ParticleBlendMode::Alpha;
1792    e.noise_module.enabled = true;
1793    e.noise_module.amplitude = 0.3;
1794    let mut col = ColorGradient::new();
1795    col.add_key(GradientKey::new(0.0, Vec4::new(0.4, 0.7, 0.1, 1.0)));
1796    col.add_key(GradientKey::new(0.5, Vec4::new(0.8, 0.5, 0.1, 1.0)));
1797    col.add_key(GradientKey::new(1.0, Vec4::new(0.6, 0.3, 0.0, 0.0)));
1798    e.color_module.color_over_lifetime = col;
1799    sys.add_emitter(e);
1800    sys
1801}
1802
1803pub fn preset_blood_splatter() -> ParticleSystem {
1804    let mut sys = ParticleSystem::new("Blood Splatter");
1805    let mut e = ParticleEmitter::new("Blood Emitter");
1806    e.shape = EmitterShape::Sphere { radius: 0.1, emit_from_shell: true };
1807    e.emission_rate = 0.0;
1808    e.emission_bursts = vec![EmissionBurst::new(0.0, 50)];
1809    e.max_particles = 100;
1810    e.lifetime_module = LifetimeModule::new(0.3, 1.0);
1811    e.velocity_module = VelocityModule::new(2.0, 8.0);
1812    e.size_module = SizeModule::new(0.02, 0.12);
1813    e.blend_mode = ParticleBlendMode::Alpha;
1814    e.gravity_module.gravity_multiplier = 2.0;
1815    e.collision_module.enabled = true;
1816    e.collision_module.bounce = 0.1;
1817    let mut col = ColorGradient::new();
1818    col.add_key(GradientKey::new(0.0, Vec4::new(0.6, 0.0, 0.0, 1.0)));
1819    col.add_key(GradientKey::new(1.0, Vec4::new(0.3, 0.0, 0.0, 0.0)));
1820    e.color_module.color_over_lifetime = col;
1821    sys.add_emitter(e);
1822    sys
1823}
1824
1825pub fn preset_vortex_portal() -> ParticleSystem {
1826    let mut sys = ParticleSystem::new("Vortex Portal");
1827    let mut e = ParticleEmitter::new("Portal Emitter");
1828    e.shape = EmitterShape::Ring { radius: 2.0, tube_radius: 0.1 };
1829    e.emission_rate = 100.0;
1830    e.max_particles = 1000;
1831    e.lifetime_module = LifetimeModule::new(1.0, 3.0);
1832    e.velocity_module = VelocityModule::new(0.1, 0.3);
1833    e.size_module = SizeModule::new(0.02, 0.1);
1834    e.blend_mode = ParticleBlendMode::Additive;
1835    let vortex_ff = ForceField::new("Portal Vortex", ForceFieldKindInner::Vortex(VortexForceField::new(Vec3::ZERO, 3.0)));
1836    sys.add_force_field(vortex_ff);
1837    let mut col = ColorGradient::new();
1838    col.add_key(GradientKey::new(0.0, Vec4::new(0.2, 0.0, 0.8, 1.0)));
1839    col.add_key(GradientKey::new(0.5, Vec4::new(0.5, 0.0, 1.0, 0.8)));
1840    col.add_key(GradientKey::new(1.0, Vec4::new(0.0, 0.0, 0.3, 0.0)));
1841    e.color_module.color_over_lifetime = col;
1842    sys.add_emitter(e);
1843    sys
1844}
1845
1846pub fn preset_healing_aura() -> ParticleSystem {
1847    let mut sys = ParticleSystem::new("Healing Aura");
1848    let mut e = ParticleEmitter::new("Heal Emitter");
1849    e.shape = EmitterShape::Ring { radius: 1.0, tube_radius: 0.05 };
1850    e.emission_rate = 30.0;
1851    e.max_particles = 200;
1852    e.lifetime_module = LifetimeModule::new(1.0, 2.0);
1853    e.velocity_module = VelocityModule::new(0.2, 0.5);
1854    e.velocity_module.velocity_offset = Vec3::new(0.0, 1.5, 0.0);
1855    e.size_module = SizeModule::new(0.05, 0.2);
1856    e.blend_mode = ParticleBlendMode::Additive;
1857    e.gravity_module.gravity_multiplier = -0.5;
1858    let mut col = ColorGradient::new();
1859    col.add_key(GradientKey::new(0.0, Vec4::new(0.0, 1.0, 0.4, 1.0)));
1860    col.add_key(GradientKey::new(0.7, Vec4::new(0.5, 1.0, 0.5, 0.7)));
1861    col.add_key(GradientKey::new(1.0, Vec4::new(1.0, 1.0, 1.0, 0.0)));
1862    e.color_module.color_over_lifetime = col;
1863    sys.add_emitter(e);
1864    sys
1865}
1866
1867pub fn preset_fireflies() -> ParticleSystem {
1868    let mut sys = ParticleSystem::new("Fireflies");
1869    let mut e = ParticleEmitter::new("Firefly Emitter");
1870    e.shape = EmitterShape::Box { half_extents: Vec3::new(5.0, 2.0, 5.0), emit_from_shell: false };
1871    e.emission_rate = 1.0;
1872    e.max_particles = 50;
1873    e.lifetime_module = LifetimeModule::new(5.0, 15.0);
1874    e.velocity_module = VelocityModule::new(0.0, 0.2);
1875    e.size_module = SizeModule::new(0.05, 0.1);
1876    e.blend_mode = ParticleBlendMode::Additive;
1877    e.noise_module.enabled = true;
1878    e.noise_module.amplitude = 0.3;
1879    e.noise_module.frequency = 0.3;
1880    let mut col = ColorGradient::new();
1881    col.add_key(GradientKey::new(0.0, Vec4::new(0.5, 1.0, 0.0, 0.0)));
1882    col.add_key(GradientKey::new(0.3, Vec4::new(0.7, 1.0, 0.2, 1.0)));
1883    col.add_key(GradientKey::new(0.7, Vec4::new(0.5, 1.0, 0.0, 1.0)));
1884    col.add_key(GradientKey::new(1.0, Vec4::new(0.3, 0.8, 0.0, 0.0)));
1885    e.color_module.color_over_lifetime = col;
1886    sys.add_emitter(e);
1887    sys
1888}
1889
1890// Missing EmitterShape variant for Cylinder used in preset above
1891impl EmitterShape {
1892    fn _cylinder_check() {}
1893}
1894
1895// ============================================================
1896// EFFECT PRESETS
1897// ============================================================
1898
1899#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1900pub enum EffectPreset {
1901    Fire,
1902    Smoke,
1903    Explosion,
1904    Rain,
1905    Sparks,
1906    MagicTrail,
1907    Snow,
1908    Dust,
1909    Bubbles,
1910    Electricity,
1911    Leaves,
1912    BloodSplatter,
1913    VortexPortal,
1914    HealingAura,
1915    Fireflies,
1916}
1917
1918impl EffectPreset {
1919    pub fn name(&self) -> &'static str {
1920        match self {
1921            EffectPreset::Fire => "Fire",
1922            EffectPreset::Smoke => "Smoke",
1923            EffectPreset::Explosion => "Explosion",
1924            EffectPreset::Rain => "Rain",
1925            EffectPreset::Sparks => "Sparks",
1926            EffectPreset::MagicTrail => "Magic Trail",
1927            EffectPreset::Snow => "Snow",
1928            EffectPreset::Dust => "Dust",
1929            EffectPreset::Bubbles => "Bubbles",
1930            EffectPreset::Electricity => "Electricity",
1931            EffectPreset::Leaves => "Falling Leaves",
1932            EffectPreset::BloodSplatter => "Blood Splatter",
1933            EffectPreset::VortexPortal => "Vortex Portal",
1934            EffectPreset::HealingAura => "Healing Aura",
1935            EffectPreset::Fireflies => "Fireflies",
1936        }
1937    }
1938    pub fn all() -> &'static [EffectPreset] {
1939        &[
1940            EffectPreset::Fire, EffectPreset::Smoke, EffectPreset::Explosion,
1941            EffectPreset::Rain, EffectPreset::Sparks, EffectPreset::MagicTrail,
1942            EffectPreset::Snow, EffectPreset::Dust, EffectPreset::Bubbles,
1943            EffectPreset::Electricity, EffectPreset::Leaves, EffectPreset::BloodSplatter,
1944            EffectPreset::VortexPortal, EffectPreset::HealingAura, EffectPreset::Fireflies,
1945        ]
1946    }
1947    pub fn create(&self) -> ParticleSystem {
1948        match self {
1949            EffectPreset::Fire => preset_fire(),
1950            EffectPreset::Smoke => preset_smoke(),
1951            EffectPreset::Explosion => preset_explosion(),
1952            EffectPreset::Rain => preset_rain(),
1953            EffectPreset::Sparks => preset_sparks(),
1954            EffectPreset::MagicTrail => preset_magic_trail(),
1955            EffectPreset::Snow => preset_snow(),
1956            EffectPreset::Dust => preset_dust(),
1957            EffectPreset::Bubbles => preset_bubbles(),
1958            EffectPreset::Electricity => preset_electricity(),
1959            EffectPreset::Leaves => preset_leaves(),
1960            EffectPreset::BloodSplatter => preset_blood_splatter(),
1961            EffectPreset::VortexPortal => preset_vortex_portal(),
1962            EffectPreset::HealingAura => preset_healing_aura(),
1963            EffectPreset::Fireflies => preset_fireflies(),
1964        }
1965    }
1966}
1967
1968// ============================================================
1969// UNDO / REDO
1970// ============================================================
1971
1972#[derive(Clone, Debug)]
1973pub enum ParticleEditorAction {
1974    AddEmitter { system_id: u64, emitter: ParticleEmitter },
1975    RemoveEmitter { system_id: u64, emitter_id: u64, emitter: ParticleEmitter },
1976    ModifyEmitter { system_id: u64, emitter_id: u64, before: Box<ParticleEmitter>, after: Box<ParticleEmitter> },
1977    AddForceField { system_id: u64, field: ForceField },
1978    RemoveForceField { system_id: u64, field_id: u64, field: ForceField },
1979    ModifyForceField { system_id: u64, field_id: u64, before: Box<ForceField>, after: Box<ForceField> },
1980    RenameSystem { system_id: u64, old_name: String, new_name: String },
1981    SetEmitterEnabled { system_id: u64, emitter_id: u64, old_state: bool, new_state: bool },
1982    SetEmitterBlend { system_id: u64, emitter_id: u64, old_mode: ParticleBlendMode, new_mode: ParticleBlendMode },
1983    SetEmissionRate { system_id: u64, emitter_id: u64, old_rate: f32, new_rate: f32 },
1984    BatchDelete { system_id: u64, emitters: Vec<ParticleEmitter> },
1985}
1986
1987impl ParticleEditorAction {
1988    pub fn description(&self) -> &'static str {
1989        match self {
1990            ParticleEditorAction::AddEmitter { .. } => "Add Emitter",
1991            ParticleEditorAction::RemoveEmitter { .. } => "Remove Emitter",
1992            ParticleEditorAction::ModifyEmitter { .. } => "Modify Emitter",
1993            ParticleEditorAction::AddForceField { .. } => "Add Force Field",
1994            ParticleEditorAction::RemoveForceField { .. } => "Remove Force Field",
1995            ParticleEditorAction::ModifyForceField { .. } => "Modify Force Field",
1996            ParticleEditorAction::RenameSystem { .. } => "Rename System",
1997            ParticleEditorAction::SetEmitterEnabled { .. } => "Toggle Emitter",
1998            ParticleEditorAction::SetEmitterBlend { .. } => "Set Blend Mode",
1999            ParticleEditorAction::SetEmissionRate { .. } => "Set Emission Rate",
2000            ParticleEditorAction::BatchDelete { .. } => "Batch Delete",
2001        }
2002    }
2003}
2004
2005#[derive(Clone, Debug)]
2006pub struct UndoRedoStack {
2007    pub undo_stack: VecDeque<ParticleEditorAction>,
2008    pub redo_stack: VecDeque<ParticleEditorAction>,
2009    pub max_history: usize,
2010}
2011
2012impl UndoRedoStack {
2013    pub fn new(max_history: usize) -> Self {
2014        Self { undo_stack: VecDeque::new(), redo_stack: VecDeque::new(), max_history }
2015    }
2016    pub fn push(&mut self, action: ParticleEditorAction) {
2017        self.redo_stack.clear();
2018        if self.undo_stack.len() >= self.max_history {
2019            self.undo_stack.pop_front();
2020        }
2021        self.undo_stack.push_back(action);
2022    }
2023    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
2024    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
2025    pub fn pop_undo(&mut self) -> Option<ParticleEditorAction> {
2026        let action = self.undo_stack.pop_back()?;
2027        self.redo_stack.push_back(action.clone());
2028        Some(action)
2029    }
2030    pub fn pop_redo(&mut self) -> Option<ParticleEditorAction> {
2031        let action = self.redo_stack.pop_back()?;
2032        self.undo_stack.push_back(action.clone());
2033        Some(action)
2034    }
2035    pub fn undo_description(&self) -> Option<&str> {
2036        self.undo_stack.back().map(|a| a.description())
2037    }
2038    pub fn redo_description(&self) -> Option<&str> {
2039        self.redo_stack.back().map(|a| a.description())
2040    }
2041    pub fn clear(&mut self) {
2042        self.undo_stack.clear();
2043        self.redo_stack.clear();
2044    }
2045}
2046
2047// ============================================================
2048// PREVIEW / SELECTION STATE
2049// ============================================================
2050
2051#[derive(Clone, Debug)]
2052pub struct PreviewState {
2053    pub is_playing: bool,
2054    pub is_paused: bool,
2055    pub playback_speed: f32,
2056    pub show_grid: bool,
2057    pub show_bounds: bool,
2058    pub show_force_fields: bool,
2059    pub show_emitter_shapes: bool,
2060    pub show_statistics: bool,
2061    pub camera_position: Vec3,
2062    pub camera_target: Vec3,
2063    pub camera_fov: f32,
2064    pub background_color: Vec4,
2065    pub wireframe: bool,
2066    pub time: f32,
2067}
2068
2069impl PreviewState {
2070    pub fn new() -> Self {
2071        Self {
2072            is_playing: false,
2073            is_paused: false,
2074            playback_speed: 1.0,
2075            show_grid: true,
2076            show_bounds: false,
2077            show_force_fields: true,
2078            show_emitter_shapes: true,
2079            show_statistics: true,
2080            camera_position: Vec3::new(0.0, 3.0, 8.0),
2081            camera_target: Vec3::ZERO,
2082            camera_fov: 60.0,
2083            background_color: Vec4::new(0.1, 0.1, 0.1, 1.0),
2084            wireframe: false,
2085            time: 0.0,
2086        }
2087    }
2088}
2089
2090#[derive(Clone, Debug)]
2091pub struct SelectionState {
2092    pub selected_emitter_ids: HashSet<u64>,
2093    pub selected_force_field_ids: HashSet<u64>,
2094    pub focused_emitter_id: Option<u64>,
2095    pub focused_force_field_id: Option<u64>,
2096    pub multi_select: bool,
2097}
2098
2099impl SelectionState {
2100    pub fn new() -> Self {
2101        Self {
2102            selected_emitter_ids: HashSet::new(),
2103            selected_force_field_ids: HashSet::new(),
2104            focused_emitter_id: None,
2105            focused_force_field_id: None,
2106            multi_select: false,
2107        }
2108    }
2109    pub fn select_emitter(&mut self, id: u64) {
2110        if !self.multi_select { self.selected_emitter_ids.clear(); }
2111        self.selected_emitter_ids.insert(id);
2112        self.focused_emitter_id = Some(id);
2113    }
2114    pub fn deselect_all(&mut self) {
2115        self.selected_emitter_ids.clear();
2116        self.selected_force_field_ids.clear();
2117        self.focused_emitter_id = None;
2118        self.focused_force_field_id = None;
2119    }
2120    pub fn is_emitter_selected(&self, id: u64) -> bool { self.selected_emitter_ids.contains(&id) }
2121}
2122
2123// ============================================================
2124// EDITOR UI STATE
2125// ============================================================
2126
2127#[derive(Clone, Copy, Debug, PartialEq)]
2128pub enum EditorTab { Emitters, ForceFields, Presets, Statistics, GpuSettings, CurveEditor, ColorPicker }
2129
2130#[derive(Clone, Copy, Debug, PartialEq)]
2131pub enum EmitterSortMode { ByName, ByParticleCount, ById, ByRenderOrder }
2132
2133#[derive(Clone, Debug)]
2134pub struct EmitterPanelState {
2135    pub filter_text: String,
2136    pub show_disabled: bool,
2137    pub sort_mode: EmitterSortMode,
2138    pub expanded_sections: HashSet<String>,
2139    pub scroll_offset: f32,
2140}
2141
2142impl EmitterPanelState {
2143    pub fn new() -> Self {
2144        Self {
2145            filter_text: String::new(),
2146            show_disabled: true,
2147            sort_mode: EmitterSortMode::ByName,
2148            expanded_sections: HashSet::new(),
2149            scroll_offset: 0.0,
2150        }
2151    }
2152}
2153
2154#[derive(Clone, Debug)]
2155pub struct PresetPanelState {
2156    pub filter_text: String,
2157    pub selected_preset: Option<EffectPreset>,
2158    pub preview_system: Option<ParticleSystem>,
2159    pub categories: Vec<String>,
2160    pub selected_category: usize,
2161}
2162
2163impl PresetPanelState {
2164    pub fn new() -> Self {
2165        Self {
2166            filter_text: String::new(),
2167            selected_preset: None,
2168            preview_system: None,
2169            categories: vec!["All".to_string(), "Fire".to_string(), "Water".to_string(), "Magic".to_string(), "Nature".to_string()],
2170            selected_category: 0,
2171        }
2172    }
2173}
2174
2175#[derive(Clone, Debug)]
2176pub struct StatisticsState {
2177    pub particle_count_history: VecDeque<f32>,
2178    pub fps_history: VecDeque<f32>,
2179    pub history_length: usize,
2180    pub show_per_emitter: bool,
2181    pub show_memory_usage: bool,
2182}
2183
2184impl StatisticsState {
2185    pub fn new() -> Self {
2186        Self {
2187            particle_count_history: VecDeque::new(),
2188            fps_history: VecDeque::new(),
2189            history_length: 120,
2190            show_per_emitter: true,
2191            show_memory_usage: true,
2192        }
2193    }
2194    pub fn push_sample(&mut self, count: f32, fps: f32) {
2195        self.particle_count_history.push_back(count);
2196        self.fps_history.push_back(fps);
2197        while self.particle_count_history.len() > self.history_length {
2198            self.particle_count_history.pop_front();
2199        }
2200        while self.fps_history.len() > self.history_length {
2201            self.fps_history.pop_front();
2202        }
2203    }
2204    pub fn average_fps(&self) -> f32 {
2205        if self.fps_history.is_empty() { return 0.0; }
2206        self.fps_history.iter().sum::<f32>() / self.fps_history.len() as f32
2207    }
2208    pub fn peak_particles(&self) -> f32 {
2209        self.particle_count_history.iter().cloned().fold(f32::NEG_INFINITY, f32::max)
2210    }
2211}
2212
2213#[derive(Clone, Debug)]
2214pub struct GpuSettingsPanelState {
2215    pub show_buffer_layout: bool,
2216    pub show_dispatch_info: bool,
2217    pub show_profiler: bool,
2218    pub selected_buffer: u32,
2219    pub simulated_particle_count: u32,
2220}
2221
2222impl GpuSettingsPanelState {
2223    pub fn new() -> Self {
2224        Self {
2225            show_buffer_layout: true,
2226            show_dispatch_info: true,
2227            show_profiler: false,
2228            selected_buffer: 0,
2229            simulated_particle_count: 10000,
2230        }
2231    }
2232}
2233
2234// ============================================================
2235// SEARCH
2236// ============================================================
2237
2238#[derive(Clone, Debug)]
2239pub enum SearchResultKind {
2240    Emitter,
2241    ForceField,
2242    Preset,
2243    Module(&'static str),
2244}
2245
2246#[derive(Clone, Debug)]
2247pub struct SearchResult {
2248    pub label: String,
2249    pub kind: SearchResultKind,
2250    pub system_id: u64,
2251    pub item_id: u64,
2252    pub relevance: f32,
2253}
2254
2255impl SearchResult {
2256    pub fn new(label: &str, kind: SearchResultKind, system_id: u64, item_id: u64, relevance: f32) -> Self {
2257        Self { label: label.to_string(), kind, system_id, item_id, relevance }
2258    }
2259}
2260
2261pub fn search_system(system: &ParticleSystem, query: &str) -> Vec<SearchResult> {
2262    let q = query.to_lowercase();
2263    let mut results = Vec::new();
2264    for e in &system.emitters {
2265        let name_lower = e.name.to_lowercase();
2266        if name_lower.contains(&q) {
2267            let relevance = if name_lower == q { 1.0 } else { 0.5 };
2268            results.push(SearchResult::new(&e.name, SearchResultKind::Emitter, system.id, e.id, relevance));
2269        }
2270    }
2271    for ff in &system.force_fields {
2272        let name_lower = ff.name.to_lowercase();
2273        if name_lower.contains(&q) {
2274            let relevance = if name_lower == q { 1.0 } else { 0.5 };
2275            results.push(SearchResult::new(&ff.name, SearchResultKind::ForceField, system.id, ff.id, relevance));
2276        }
2277    }
2278    for preset in EffectPreset::all() {
2279        let pname = preset.name().to_lowercase();
2280        if pname.contains(&q) {
2281            results.push(SearchResult::new(preset.name(), SearchResultKind::Preset, 0, *preset as u64, 0.3));
2282        }
2283    }
2284    results.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap());
2285    results
2286}
2287
2288// ============================================================
2289// FORCE FIELD KIND ENUM (for editor panels)
2290// ============================================================
2291
2292#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2293pub enum ForceFieldKind {
2294    Directional,
2295    Vortex,
2296    Turbulence,
2297    Drag,
2298    GravityPoint,
2299    Wind,
2300    Magnetic,
2301}
2302
2303impl ForceFieldKind {
2304    pub fn name(&self) -> &'static str {
2305        match self {
2306            ForceFieldKind::Directional => "Directional",
2307            ForceFieldKind::Vortex => "Vortex",
2308            ForceFieldKind::Turbulence => "Turbulence",
2309            ForceFieldKind::Drag => "Drag",
2310            ForceFieldKind::GravityPoint => "Gravity Point",
2311            ForceFieldKind::Wind => "Wind",
2312            ForceFieldKind::Magnetic => "Magnetic (Lorentz)",
2313        }
2314    }
2315    pub fn all() -> &'static [ForceFieldKind] {
2316        &[
2317            ForceFieldKind::Directional, ForceFieldKind::Vortex, ForceFieldKind::Turbulence,
2318            ForceFieldKind::Drag, ForceFieldKind::GravityPoint, ForceFieldKind::Wind,
2319            ForceFieldKind::Magnetic,
2320        ]
2321    }
2322    pub fn create_default(&self) -> ForceFieldKindInner {
2323        match self {
2324            ForceFieldKind::Directional => ForceFieldKindInner::Directional(DirectionalForce::new(Vec3::Y, 1.0)),
2325            ForceFieldKind::Vortex => ForceFieldKindInner::Vortex(VortexForceField::new(Vec3::ZERO, 2.0)),
2326            ForceFieldKind::Turbulence => ForceFieldKindInner::Turbulence(TurbulenceForce::new(1.0, 1.0)),
2327            ForceFieldKind::Drag => ForceFieldKindInner::Drag(DragForce::new(0.1)),
2328            ForceFieldKind::GravityPoint => ForceFieldKindInner::GravityPoint(GravityPointForce::new(Vec3::ZERO, 5.0)),
2329            ForceFieldKind::Wind => ForceFieldKindInner::Wind(WindForce::new(Vec3::X, 2.0)),
2330            ForceFieldKind::Magnetic => ForceFieldKindInner::Magnetic(MagneticForce::new(Vec3::Z * 1.0, 1.0)),
2331        }
2332    }
2333}
2334
2335// ============================================================
2336// CURVE EDITOR STATE
2337// ============================================================
2338
2339#[derive(Clone, Copy, Debug, PartialEq)]
2340pub enum TangentMode { Auto, Free, Linear, Flat, Stepped }
2341
2342#[derive(Clone, Debug)]
2343pub struct CurveEditorState {
2344    pub target_curve_id: Option<u64>,
2345    pub selected_key_indices: HashSet<usize>,
2346    pub tangent_mode: TangentMode,
2347    pub show_tangents: bool,
2348    pub snap_x: bool,
2349    pub snap_y: bool,
2350    pub snap_x_increment: f32,
2351    pub snap_y_increment: f32,
2352    pub view_min: Vec2,
2353    pub view_max: Vec2,
2354    pub dragging_key: Option<usize>,
2355    pub drag_start: Vec2,
2356}
2357
2358impl CurveEditorState {
2359    pub fn new() -> Self {
2360        Self {
2361            target_curve_id: None,
2362            selected_key_indices: HashSet::new(),
2363            tangent_mode: TangentMode::Auto,
2364            show_tangents: true,
2365            snap_x: false,
2366            snap_y: false,
2367            snap_x_increment: 0.1,
2368            snap_y_increment: 0.1,
2369            view_min: Vec2::new(0.0, -1.0),
2370            view_max: Vec2::new(1.0, 2.0),
2371            dragging_key: None,
2372            drag_start: Vec2::ZERO,
2373        }
2374    }
2375    pub fn fit_to_curve(&mut self, curve: &FloatCurve) {
2376        if curve.keys.is_empty() { return; }
2377        let min_t = curve.keys.first().unwrap().time;
2378        let max_t = curve.keys.last().unwrap().time;
2379        let min_v = curve.keys.iter().map(|k| k.value).fold(f32::INFINITY, f32::min);
2380        let max_v = curve.keys.iter().map(|k| k.value).fold(f32::NEG_INFINITY, f32::max);
2381        let pad_t = (max_t - min_t) * 0.1;
2382        let pad_v = ((max_v - min_v) * 0.1).max(0.1);
2383        self.view_min = Vec2::new(min_t - pad_t, min_v - pad_v);
2384        self.view_max = Vec2::new(max_t + pad_t, max_v + pad_v);
2385    }
2386    pub fn auto_tangent(keys: &mut Vec<CurveKey>, idx: usize) {
2387        if keys.len() < 2 { return; }
2388        let tangent = if idx == 0 {
2389            let next = &keys[1];
2390            (next.value - keys[0].value) / (next.time - keys[0].time).max(0.001)
2391        } else if idx == keys.len() - 1 {
2392            let prev = &keys[idx - 1];
2393            let cur = &keys[idx];
2394            (cur.value - prev.value) / (cur.time - prev.time).max(0.001)
2395        } else {
2396            let prev = &keys[idx - 1];
2397            let next = &keys[idx + 1];
2398            (next.value - prev.value) / (next.time - prev.time).max(0.001)
2399        };
2400        keys[idx].in_tangent = tangent;
2401        keys[idx].out_tangent = tangent;
2402    }
2403}
2404
2405// ============================================================
2406// COLOR PICKER STATE
2407// ============================================================
2408
2409#[derive(Clone, Copy, Debug, PartialEq)]
2410pub enum ColorPickerMode { RGB, HSV, HSL, Hex }
2411
2412#[derive(Clone, Debug)]
2413pub struct ColorPickerState {
2414    pub current_color: Vec4,
2415    pub previous_color: Vec4,
2416    pub mode: ColorPickerMode,
2417    pub hex_input: String,
2418    pub show_alpha: bool,
2419    pub eyedropper_active: bool,
2420    pub palette: Vec<Vec4>,
2421}
2422
2423impl ColorPickerState {
2424    pub fn new() -> Self {
2425        Self {
2426            current_color: Vec4::ONE,
2427            previous_color: Vec4::ONE,
2428            mode: ColorPickerMode::HSV,
2429            hex_input: String::from("FFFFFFFF"),
2430            show_alpha: true,
2431            eyedropper_active: false,
2432            palette: vec![
2433                Vec4::new(1.0, 0.0, 0.0, 1.0),
2434                Vec4::new(0.0, 1.0, 0.0, 1.0),
2435                Vec4::new(0.0, 0.0, 1.0, 1.0),
2436                Vec4::new(1.0, 1.0, 0.0, 1.0),
2437                Vec4::new(1.0, 0.0, 1.0, 1.0),
2438                Vec4::new(0.0, 1.0, 1.0, 1.0),
2439                Vec4::ONE,
2440                Vec4::new(0.0, 0.0, 0.0, 1.0),
2441            ],
2442        }
2443    }
2444
2445    pub fn rgb_to_hsv(rgb: Vec3) -> Vec3 {
2446        let r = rgb.x; let g = rgb.y; let b = rgb.z;
2447        let max = r.max(g).max(b);
2448        let min = r.min(g).min(b);
2449        let delta = max - min;
2450        let h = if delta < 0.0001 {
2451            0.0
2452        } else if max == r {
2453            60.0 * ((g - b) / delta % 6.0)
2454        } else if max == g {
2455            60.0 * ((b - r) / delta + 2.0)
2456        } else {
2457            60.0 * ((r - g) / delta + 4.0)
2458        };
2459        let h = if h < 0.0 { h + 360.0 } else { h };
2460        let s = if max < 0.0001 { 0.0 } else { delta / max };
2461        Vec3::new(h, s, max)
2462    }
2463
2464    pub fn hsv_to_rgb(hsv: Vec3) -> Vec3 {
2465        let h = hsv.x; let s = hsv.y; let v = hsv.z;
2466        let c = v * s;
2467        let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
2468        let m = v - c;
2469        let (r, g, b) = if h < 60.0 { (c, x, 0.0) }
2470            else if h < 120.0 { (x, c, 0.0) }
2471            else if h < 180.0 { (0.0, c, x) }
2472            else if h < 240.0 { (0.0, x, c) }
2473            else if h < 300.0 { (x, 0.0, c) }
2474            else { (c, 0.0, x) };
2475        Vec3::new(r + m, g + m, b + m)
2476    }
2477
2478    pub fn color_to_hex(color: Vec4) -> String {
2479        let r = (color.x.clamp(0.0, 1.0) * 255.0) as u8;
2480        let g = (color.y.clamp(0.0, 1.0) * 255.0) as u8;
2481        let b = (color.z.clamp(0.0, 1.0) * 255.0) as u8;
2482        let a = (color.w.clamp(0.0, 1.0) * 255.0) as u8;
2483        format!("{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
2484    }
2485
2486    pub fn hex_to_color(hex: &str) -> Option<Vec4> {
2487        let hex = hex.trim_start_matches('#');
2488        if hex.len() == 6 {
2489            let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
2490            let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
2491            let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
2492            Some(Vec4::new(r, g, b, 1.0))
2493        } else if hex.len() == 8 {
2494            let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.0;
2495            let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.0;
2496            let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.0;
2497            let a = u8::from_str_radix(&hex[6..8], 16).ok()? as f32 / 255.0;
2498            Some(Vec4::new(r, g, b, a))
2499        } else {
2500            None
2501        }
2502    }
2503}
2504
2505// ============================================================
2506// NOTIFICATIONS
2507// ============================================================
2508
2509#[derive(Clone, Copy, Debug, PartialEq)]
2510pub enum NotificationKind { Info, Warning, Error, Success }
2511
2512#[derive(Clone, Debug)]
2513pub struct Notification {
2514    pub id: u64,
2515    pub kind: NotificationKind,
2516    pub message: String,
2517    pub duration: f32,
2518    pub elapsed: f32,
2519    pub dismissed: bool,
2520}
2521
2522impl Notification {
2523    pub fn new(id: u64, kind: NotificationKind, message: &str, duration: f32) -> Self {
2524        Self { id, kind, message: message.to_string(), duration, elapsed: 0.0, dismissed: false }
2525    }
2526    pub fn is_expired(&self) -> bool { self.elapsed >= self.duration || self.dismissed }
2527    pub fn opacity(&self) -> f32 {
2528        let fade_time = 0.5_f32.min(self.duration * 0.2);
2529        let remaining = self.duration - self.elapsed;
2530        if remaining < fade_time { remaining / fade_time } else { 1.0 }
2531    }
2532}
2533
2534#[derive(Clone, Debug)]
2535pub struct NotificationCenter {
2536    pub notifications: VecDeque<Notification>,
2537    pub next_id: u64,
2538    pub max_visible: usize,
2539}
2540
2541impl NotificationCenter {
2542    pub fn new() -> Self { Self { notifications: VecDeque::new(), next_id: 1, max_visible: 5 } }
2543    pub fn push(&mut self, kind: NotificationKind, message: &str, duration: f32) -> u64 {
2544        let id = self.next_id;
2545        self.next_id += 1;
2546        self.notifications.push_back(Notification::new(id, kind, message, duration));
2547        if self.notifications.len() > self.max_visible * 2 {
2548            self.notifications.pop_front();
2549        }
2550        id
2551    }
2552    pub fn info(&mut self, msg: &str) -> u64 { self.push(NotificationKind::Info, msg, 3.0) }
2553    pub fn warn(&mut self, msg: &str) -> u64 { self.push(NotificationKind::Warning, msg, 5.0) }
2554    pub fn error(&mut self, msg: &str) -> u64 { self.push(NotificationKind::Error, msg, 8.0) }
2555    pub fn success(&mut self, msg: &str) -> u64 { self.push(NotificationKind::Success, msg, 3.0) }
2556    pub fn update(&mut self, dt: f32) {
2557        for n in &mut self.notifications { n.elapsed += dt; }
2558        self.notifications.retain(|n| !n.is_expired());
2559    }
2560    pub fn dismiss(&mut self, id: u64) {
2561        if let Some(n) = self.notifications.iter_mut().find(|n| n.id == id) {
2562            n.dismissed = true;
2563        }
2564    }
2565}
2566
2567// ============================================================
2568// PRESET MANAGER
2569// ============================================================
2570
2571#[derive(Clone, Debug)]
2572pub struct PresetManager {
2573    pub custom_presets: HashMap<String, ParticleSystem>,
2574    pub builtin_presets: Vec<EffectPreset>,
2575    pub favorites: HashSet<String>,
2576    pub recently_used: VecDeque<String>,
2577    pub max_recent: usize,
2578}
2579
2580impl PresetManager {
2581    pub fn new() -> Self {
2582        Self {
2583            custom_presets: HashMap::new(),
2584            builtin_presets: EffectPreset::all().to_vec(),
2585            favorites: HashSet::new(),
2586            recently_used: VecDeque::new(),
2587            max_recent: 10,
2588        }
2589    }
2590    pub fn save_custom(&mut self, name: &str, system: ParticleSystem) {
2591        self.custom_presets.insert(name.to_string(), system);
2592        self.mark_used(name);
2593    }
2594    pub fn load(&self, name: &str) -> Option<ParticleSystem> {
2595        if let Some(sys) = self.custom_presets.get(name) {
2596            return Some(sys.clone());
2597        }
2598        for p in &self.builtin_presets {
2599            if p.name() == name {
2600                return Some(p.create());
2601            }
2602        }
2603        None
2604    }
2605    pub fn toggle_favorite(&mut self, name: &str) {
2606        if self.favorites.contains(name) {
2607            self.favorites.remove(name);
2608        } else {
2609            self.favorites.insert(name.to_string());
2610        }
2611    }
2612    pub fn mark_used(&mut self, name: &str) {
2613        self.recently_used.retain(|n| n != name);
2614        self.recently_used.push_front(name.to_string());
2615        while self.recently_used.len() > self.max_recent {
2616            self.recently_used.pop_back();
2617        }
2618    }
2619    pub fn all_names(&self) -> Vec<String> {
2620        let mut names: Vec<String> = self.builtin_presets.iter().map(|p| p.name().to_string()).collect();
2621        names.extend(self.custom_presets.keys().cloned());
2622        names.sort();
2623        names
2624    }
2625}
2626
2627// ============================================================
2628// RENDERER DRAW CALL
2629// ============================================================
2630
2631#[derive(Clone, Debug)]
2632pub struct RendererDrawCall {
2633    pub emitter_id: u64,
2634    pub particle_count: u32,
2635    pub texture_id: u64,
2636    pub material_id: u64,
2637    pub blend_mode: ParticleBlendMode,
2638    pub render_mode: ParticleRenderMode,
2639    pub sort_mode: SortMode,
2640    pub render_order: i32,
2641    pub bounds_min: Vec3,
2642    pub bounds_max: Vec3,
2643}
2644
2645#[derive(Clone, Debug)]
2646pub struct ParticleRenderer {
2647    pub draw_calls: Vec<RendererDrawCall>,
2648    pub total_drawn: u32,
2649    pub culled_count: u32,
2650    pub sort_key_buffer: Vec<(f32, usize)>,
2651}
2652
2653impl ParticleRenderer {
2654    pub fn new() -> Self {
2655        Self { draw_calls: Vec::new(), total_drawn: 0, culled_count: 0, sort_key_buffer: Vec::new() }
2656    }
2657    pub fn clear(&mut self) {
2658        self.draw_calls.clear();
2659        self.total_drawn = 0;
2660        self.culled_count = 0;
2661        self.sort_key_buffer.clear();
2662    }
2663    pub fn add_draw_call(&mut self, call: RendererDrawCall) {
2664        self.total_drawn += call.particle_count;
2665        self.draw_calls.push(call);
2666    }
2667    pub fn sort_draw_calls(&mut self) {
2668        self.draw_calls.sort_by(|a, b| a.render_order.cmp(&b.render_order));
2669    }
2670    pub fn compute_bounds(&self, particles: &[Particle]) -> (Vec3, Vec3) {
2671        if particles.is_empty() { return (Vec3::ZERO, Vec3::ZERO); }
2672        let mut min = Vec3::splat(f32::INFINITY);
2673        let mut max = Vec3::splat(f32::NEG_INFINITY);
2674        for p in particles {
2675            min = min.min(p.position - Vec3::splat(p.size));
2676            max = max.max(p.position + Vec3::splat(p.size));
2677        }
2678        (min, max)
2679    }
2680    pub fn prepare_for_system(&mut self, system: &ParticleSystem, camera_pos: Vec3) {
2681        self.clear();
2682        for emitter in &system.emitters {
2683            if !emitter.enabled { self.culled_count += emitter.particles.len() as u32; continue; }
2684            let (bounds_min, bounds_max) = self.compute_bounds(&emitter.particles);
2685            let call = RendererDrawCall {
2686                emitter_id: emitter.id,
2687                particle_count: emitter.particles.len() as u32,
2688                texture_id: emitter.texture_id,
2689                material_id: emitter.material_id,
2690                blend_mode: emitter.blend_mode,
2691                render_mode: emitter.render_mode,
2692                sort_mode: emitter.sort_mode,
2693                render_order: emitter.render_order,
2694                bounds_min,
2695                bounds_max,
2696            };
2697            self.add_draw_call(call);
2698        }
2699        self.sort_draw_calls();
2700    }
2701}
2702
2703// ============================================================
2704// KEYBOARD SHORTCUTS
2705// ============================================================
2706
2707#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2708pub enum EditorCommand {
2709    Undo, Redo, Save, SaveAs, New, Open, Delete, Duplicate, Copy, Paste,
2710    Play, Stop, Pause, Reset, SelectAll, DeselectAll, FocusSelected,
2711    ToggleGrid, ToggleBounds, ToggleStatistics, ToggleForceFields,
2712    ZoomIn, ZoomOut, ResetCamera, FrameSelected,
2713    AddEmitter, AddForceField, RenameSelected,
2714    OpenPresets, OpenSettings, ToggleCurveEditor,
2715}
2716
2717#[derive(Clone, Debug)]
2718pub struct KeyShortcut {
2719    pub command: EditorCommand,
2720    pub key: u32,
2721    pub ctrl: bool,
2722    pub shift: bool,
2723    pub alt: bool,
2724    pub label: &'static str,
2725}
2726
2727impl KeyShortcut {
2728    pub fn new(command: EditorCommand, key: u32, ctrl: bool, shift: bool, alt: bool, label: &'static str) -> Self {
2729        Self { command, key, ctrl, shift, alt, label }
2730    }
2731}
2732
2733pub fn default_shortcuts() -> Vec<KeyShortcut> {
2734    vec![
2735        KeyShortcut::new(EditorCommand::Undo, b'Z' as u32, true, false, false, "Ctrl+Z"),
2736        KeyShortcut::new(EditorCommand::Redo, b'Y' as u32, true, false, false, "Ctrl+Y"),
2737        KeyShortcut::new(EditorCommand::Save, b'S' as u32, true, false, false, "Ctrl+S"),
2738        KeyShortcut::new(EditorCommand::SaveAs, b'S' as u32, true, true, false, "Ctrl+Shift+S"),
2739        KeyShortcut::new(EditorCommand::New, b'N' as u32, true, false, false, "Ctrl+N"),
2740        KeyShortcut::new(EditorCommand::Open, b'O' as u32, true, false, false, "Ctrl+O"),
2741        KeyShortcut::new(EditorCommand::Delete, 46, false, false, false, "Del"),
2742        KeyShortcut::new(EditorCommand::Duplicate, b'D' as u32, true, false, false, "Ctrl+D"),
2743        KeyShortcut::new(EditorCommand::Copy, b'C' as u32, true, false, false, "Ctrl+C"),
2744        KeyShortcut::new(EditorCommand::Paste, b'V' as u32, true, false, false, "Ctrl+V"),
2745        KeyShortcut::new(EditorCommand::Play, 112, false, false, false, "F5"),
2746        KeyShortcut::new(EditorCommand::Stop, 113, false, false, false, "F6"),
2747        KeyShortcut::new(EditorCommand::Pause, 114, false, false, false, "F7"),
2748        KeyShortcut::new(EditorCommand::Reset, 115, false, false, false, "F8"),
2749        KeyShortcut::new(EditorCommand::SelectAll, b'A' as u32, true, false, false, "Ctrl+A"),
2750        KeyShortcut::new(EditorCommand::ToggleGrid, b'G' as u32, false, false, false, "G"),
2751        KeyShortcut::new(EditorCommand::ToggleStatistics, b'T' as u32, false, false, false, "T"),
2752        KeyShortcut::new(EditorCommand::ResetCamera, b'R' as u32, false, false, false, "R"),
2753        KeyShortcut::new(EditorCommand::FrameSelected, b'F' as u32, false, false, false, "F"),
2754        KeyShortcut::new(EditorCommand::AddEmitter, b'E' as u32, true, false, false, "Ctrl+E"),
2755        KeyShortcut::new(EditorCommand::AddForceField, b'F' as u32, true, false, false, "Ctrl+F"),
2756        KeyShortcut::new(EditorCommand::OpenPresets, b'P' as u32, true, false, false, "Ctrl+P"),
2757    ]
2758}
2759
2760// ============================================================
2761// RECENT FILES
2762// ============================================================
2763
2764#[derive(Clone, Debug)]
2765pub struct RecentFileEntry {
2766    pub path: String,
2767    pub name: String,
2768    pub accessed_at: u64,
2769    pub thumbnail_id: Option<u64>,
2770}
2771
2772#[derive(Clone, Debug)]
2773pub struct RecentFiles {
2774    pub entries: VecDeque<RecentFileEntry>,
2775    pub max_entries: usize,
2776}
2777
2778impl RecentFiles {
2779    pub fn new() -> Self { Self { entries: VecDeque::new(), max_entries: 20 } }
2780    pub fn push(&mut self, path: &str, name: &str, time: u64) {
2781        self.entries.retain(|e| e.path != path);
2782        self.entries.push_front(RecentFileEntry {
2783            path: path.to_string(),
2784            name: name.to_string(),
2785            accessed_at: time,
2786            thumbnail_id: None,
2787        });
2788        while self.entries.len() > self.max_entries { self.entries.pop_back(); }
2789    }
2790    pub fn clear(&mut self) { self.entries.clear(); }
2791}
2792
2793// ============================================================
2794// EXPORT
2795// ============================================================
2796
2797#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2798pub enum ExportFormat {
2799    Json,
2800    Binary,
2801    Csv,
2802    UnityParticleSystem,
2803    UnrealNiagara,
2804}
2805
2806impl ExportFormat {
2807    pub fn extension(&self) -> &'static str {
2808        match self {
2809            ExportFormat::Json => "json",
2810            ExportFormat::Binary => "bin",
2811            ExportFormat::Csv => "csv",
2812            ExportFormat::UnityParticleSystem => "prefab",
2813            ExportFormat::UnrealNiagara => "uasset",
2814        }
2815    }
2816    pub fn name(&self) -> &'static str {
2817        match self {
2818            ExportFormat::Json => "JSON",
2819            ExportFormat::Binary => "Binary",
2820            ExportFormat::Csv => "CSV",
2821            ExportFormat::UnityParticleSystem => "Unity Particle System",
2822            ExportFormat::UnrealNiagara => "Unreal Niagara",
2823        }
2824    }
2825}
2826
2827#[derive(Clone, Debug)]
2828pub struct ExportOptions {
2829    pub format: ExportFormat,
2830    pub path: String,
2831    pub include_previews: bool,
2832    pub compress: bool,
2833    pub include_textures: bool,
2834    pub pretty_print: bool,
2835}
2836
2837impl ExportOptions {
2838    pub fn new(format: ExportFormat, path: &str) -> Self {
2839        Self {
2840            format,
2841            path: path.to_string(),
2842            include_previews: false,
2843            compress: false,
2844            include_textures: false,
2845            pretty_print: true,
2846        }
2847    }
2848}
2849
2850// ============================================================
2851// SIMULATION RUNNER
2852// ============================================================
2853
2854#[derive(Clone, Debug)]
2855pub struct SimulationRunner {
2856    pub time_step: f32,
2857    pub fixed_time_step: bool,
2858    pub max_substeps: u32,
2859    pub accumulated_time: f32,
2860    pub total_time: f32,
2861    pub frame_count: u64,
2862}
2863
2864impl SimulationRunner {
2865    pub fn new() -> Self {
2866        Self {
2867            time_step: 1.0 / 60.0,
2868            fixed_time_step: true,
2869            max_substeps: 4,
2870            accumulated_time: 0.0,
2871            total_time: 0.0,
2872            frame_count: 0,
2873        }
2874    }
2875    pub fn step(&mut self, delta: f32, system: &mut ParticleSystem) {
2876        if self.fixed_time_step {
2877            self.accumulated_time += delta;
2878            let mut substeps = 0;
2879            while self.accumulated_time >= self.time_step && substeps < self.max_substeps {
2880                system.update(self.time_step);
2881                self.accumulated_time -= self.time_step;
2882                self.total_time += self.time_step;
2883                substeps += 1;
2884            }
2885        } else {
2886            system.update(delta);
2887            self.total_time += delta;
2888        }
2889        self.frame_count += 1;
2890    }
2891    pub fn reset(&mut self) {
2892        self.accumulated_time = 0.0;
2893        self.total_time = 0.0;
2894        self.frame_count = 0;
2895    }
2896}
2897
2898// ============================================================
2899// EMITTER INSPECTOR STATE
2900// ============================================================
2901
2902#[derive(Clone, Debug)]
2903pub struct EmitterInspectorState {
2904    pub active_tab: EmitterInspectorTab,
2905    pub expanded_modules: HashSet<String>,
2906    pub module_search: String,
2907    pub show_advanced: bool,
2908}
2909
2910#[derive(Clone, Copy, Debug, PartialEq)]
2911pub enum EmitterInspectorTab {
2912    General,
2913    Emission,
2914    Modules,
2915    Rendering,
2916    Physics,
2917    Lod,
2918    GpuSettings,
2919}
2920
2921impl EmitterInspectorState {
2922    pub fn new() -> Self {
2923        let mut expanded = HashSet::new();
2924        expanded.insert("Lifetime".to_string());
2925        expanded.insert("Velocity".to_string());
2926        expanded.insert("Color".to_string());
2927        Self {
2928            active_tab: EmitterInspectorTab::General,
2929            expanded_modules: expanded,
2930            module_search: String::new(),
2931            show_advanced: false,
2932        }
2933    }
2934    pub fn toggle_module(&mut self, name: &str) {
2935        if self.expanded_modules.contains(name) {
2936            self.expanded_modules.remove(name);
2937        } else {
2938            self.expanded_modules.insert(name.to_string());
2939        }
2940    }
2941    pub fn is_expanded(&self, name: &str) -> bool { self.expanded_modules.contains(name) }
2942}
2943
2944// ============================================================
2945// VALIDATION
2946// ============================================================
2947
2948#[derive(Clone, Debug)]
2949pub struct ValidationError { pub message: String, pub emitter_id: Option<u64> }
2950#[derive(Clone, Debug)]
2951pub struct ValidationWarning { pub message: String, pub emitter_id: Option<u64> }
2952
2953#[derive(Clone, Debug)]
2954pub struct ValidationResult {
2955    pub errors: Vec<ValidationError>,
2956    pub warnings: Vec<ValidationWarning>,
2957}
2958
2959impl ValidationResult {
2960    pub fn new() -> Self { Self { errors: Vec::new(), warnings: Vec::new() } }
2961    pub fn is_valid(&self) -> bool { self.errors.is_empty() }
2962    pub fn add_error(&mut self, msg: &str, id: Option<u64>) {
2963        self.errors.push(ValidationError { message: msg.to_string(), emitter_id: id });
2964    }
2965    pub fn add_warning(&mut self, msg: &str, id: Option<u64>) {
2966        self.warnings.push(ValidationWarning { message: msg.to_string(), emitter_id: id });
2967    }
2968}
2969
2970pub fn validate_emitter(e: &ParticleEmitter) -> ValidationResult {
2971    let mut r = ValidationResult::new();
2972    if e.max_particles == 0 { r.add_error("Max particles is 0", Some(e.id)); }
2973    if e.lifetime_module.min_lifetime <= 0.0 { r.add_error("Min lifetime must be > 0", Some(e.id)); }
2974    if e.lifetime_module.min_lifetime > e.lifetime_module.max_lifetime {
2975        r.add_error("Min lifetime > max lifetime", Some(e.id));
2976    }
2977    if e.emission_rate < 0.0 { r.add_error("Emission rate cannot be negative", Some(e.id)); }
2978    if e.emission_rate == 0.0 && e.emission_bursts.is_empty() {
2979        r.add_warning("Emitter has no emission rate and no bursts", Some(e.id));
2980    }
2981    if e.size_module.start_size_min <= 0.0 { r.add_warning("Min start size is 0 or negative", Some(e.id)); }
2982    if e.velocity_module.initial_speed_min > e.velocity_module.initial_speed_max {
2983        r.add_error("Min speed > max speed", Some(e.id));
2984    }
2985    if e.max_particles > 100000 { r.add_warning("Very high max particle count (>100k) may impact performance", Some(e.id)); }
2986    r
2987}
2988
2989pub fn validate_system(sys: &ParticleSystem) -> ValidationResult {
2990    let mut r = ValidationResult::new();
2991    if sys.emitters.is_empty() { r.add_warning("System has no emitters", None); }
2992    for e in &sys.emitters {
2993        let er = validate_emitter(e);
2994        r.errors.extend(er.errors);
2995        r.warnings.extend(er.warnings);
2996    }
2997    let total_max: u32 = sys.emitters.iter().map(|e| e.max_particles).sum();
2998    if total_max > 500000 { r.add_warning("Total max particles across all emitters is very high", None); }
2999    r
3000}
3001
3002// ============================================================
3003// BATCH OPERATIONS
3004// ============================================================
3005
3006pub fn batch_set_enabled(system: &mut ParticleSystem, ids: &[u64], enabled: bool) {
3007    for e in &mut system.emitters {
3008        if ids.contains(&e.id) { e.enabled = enabled; }
3009    }
3010}
3011
3012pub fn batch_delete(system: &mut ParticleSystem, ids: &[u64]) -> Vec<ParticleEmitter> {
3013    let mut removed = Vec::new();
3014    let mut kept = Vec::new();
3015    for e in system.emitters.drain(..) {
3016        if ids.contains(&e.id) { removed.push(e); } else { kept.push(e); }
3017    }
3018    system.emitters = kept;
3019    removed
3020}
3021
3022pub fn batch_set_blend(system: &mut ParticleSystem, ids: &[u64], mode: ParticleBlendMode) {
3023    for e in &mut system.emitters {
3024        if ids.contains(&e.id) { e.blend_mode = mode; }
3025    }
3026}
3027
3028pub fn batch_scale_count(system: &mut ParticleSystem, ids: &[u64], scale: f32) {
3029    for e in &mut system.emitters {
3030        if ids.contains(&e.id) {
3031            e.max_particles = ((e.max_particles as f32 * scale) as u32).max(1);
3032        }
3033    }
3034}
3035
3036pub fn batch_set_emission_rate(system: &mut ParticleSystem, ids: &[u64], rate: f32) {
3037    for e in &mut system.emitters {
3038        if ids.contains(&e.id) { e.emission_rate = rate.max(0.0); }
3039    }
3040}
3041
3042pub fn batch_duplicate_emitters(system: &mut ParticleSystem, ids: &[u64]) -> Vec<u64> {
3043    let originals: Vec<ParticleEmitter> = system.emitters.iter()
3044        .filter(|e| ids.contains(&e.id))
3045        .cloned()
3046        .collect();
3047    let mut new_ids = Vec::new();
3048    for mut e in originals {
3049        let new_id = system.emitters.len() as u64 + 1 + new_ids.len() as u64;
3050        e.id = new_id;
3051        e.name = format!("{} (Copy)", e.name);
3052        new_ids.push(new_id);
3053        system.emitters.push(e);
3054    }
3055    new_ids
3056}
3057
3058// ============================================================
3059// HELPER CURVES / GRADIENTS
3060// ============================================================
3061
3062pub fn velocity_ease_in_out() -> FloatCurve {
3063    let mut c = FloatCurve::new();
3064    c.add_key(CurveKey::with_tangents(0.0, 0.0, 0.0, 0.0));
3065    c.add_key(CurveKey::with_tangents(0.5, 1.0, 2.0, 2.0));
3066    c.add_key(CurveKey::with_tangents(1.0, 0.0, 0.0, 0.0));
3067    c
3068}
3069
3070pub fn size_burst_curve() -> FloatCurve {
3071    let mut c = FloatCurve::new();
3072    c.add_key(CurveKey::with_tangents(0.0, 0.0, 0.0, 3.0));
3073    c.add_key(CurveKey::with_tangents(0.1, 1.2, 3.0, -1.0));
3074    c.add_key(CurveKey::with_tangents(0.5, 1.0, 0.0, 0.0));
3075    c.add_key(CurveKey::with_tangents(1.0, 0.0, -1.0, 0.0));
3076    c
3077}
3078
3079pub fn alpha_fade_gradient() -> ColorGradient {
3080    let mut g = ColorGradient::new();
3081    g.add_key(GradientKey::new(0.0, Vec4::new(1.0, 1.0, 1.0, 0.0)));
3082    g.add_key(GradientKey::new(0.1, Vec4::new(1.0, 1.0, 1.0, 1.0)));
3083    g.add_key(GradientKey::new(0.8, Vec4::new(1.0, 1.0, 1.0, 1.0)));
3084    g.add_key(GradientKey::new(1.0, Vec4::new(1.0, 1.0, 1.0, 0.0)));
3085    g
3086}
3087
3088pub fn fire_gradient() -> ColorGradient {
3089    let mut g = ColorGradient::new();
3090    g.add_key(GradientKey::new(0.0, Vec4::new(1.0, 1.0, 0.5, 1.0)));
3091    g.add_key(GradientKey::new(0.3, Vec4::new(1.0, 0.5, 0.0, 1.0)));
3092    g.add_key(GradientKey::new(0.7, Vec4::new(0.5, 0.1, 0.0, 0.8)));
3093    g.add_key(GradientKey::new(1.0, Vec4::new(0.1, 0.0, 0.0, 0.0)));
3094    g
3095}
3096
3097pub fn rainbow_gradient() -> ColorGradient {
3098    let mut g = ColorGradient::new();
3099    g.add_key(GradientKey::new(0.0,    Vec4::new(1.0, 0.0, 0.0, 1.0)));
3100    g.add_key(GradientKey::new(0.166,  Vec4::new(1.0, 0.5, 0.0, 1.0)));
3101    g.add_key(GradientKey::new(0.333,  Vec4::new(1.0, 1.0, 0.0, 1.0)));
3102    g.add_key(GradientKey::new(0.5,    Vec4::new(0.0, 1.0, 0.0, 1.0)));
3103    g.add_key(GradientKey::new(0.666,  Vec4::new(0.0, 0.0, 1.0, 1.0)));
3104    g.add_key(GradientKey::new(0.833,  Vec4::new(0.5, 0.0, 1.0, 1.0)));
3105    g.add_key(GradientKey::new(1.0,    Vec4::new(1.0, 0.0, 0.0, 1.0)));
3106    g
3107}
3108
3109pub fn cool_to_warm_gradient() -> ColorGradient {
3110    let mut g = ColorGradient::new();
3111    g.add_key(GradientKey::new(0.0, Vec4::new(0.0, 0.2, 1.0, 1.0)));
3112    g.add_key(GradientKey::new(0.5, Vec4::new(1.0, 1.0, 1.0, 1.0)));
3113    g.add_key(GradientKey::new(1.0, Vec4::new(1.0, 0.1, 0.0, 1.0)));
3114    g
3115}
3116
3117pub fn pulse_curve(frequency: f32) -> FloatCurve {
3118    let mut c = FloatCurve::new();
3119    let steps = (frequency * 8.0) as u32;
3120    for i in 0..=steps {
3121        let t = i as f32 / steps as f32;
3122        let v = (t * frequency * std::f32::consts::TAU).sin() * 0.5 + 0.5;
3123        c.add_key(CurveKey::new(t, v));
3124    }
3125    c
3126}
3127
3128// ============================================================
3129// TEXTURE ATLAS
3130// ============================================================
3131
3132#[derive(Clone, Debug)]
3133pub struct TextureAtlasEntry {
3134    pub id: u64,
3135    pub name: String,
3136    pub uv_min: Vec2,
3137    pub uv_max: Vec2,
3138    pub frame_count: u32,
3139    pub fps: f32,
3140}
3141
3142#[derive(Clone, Debug)]
3143pub struct TextureAtlas {
3144    pub id: u64,
3145    pub width: u32,
3146    pub height: u32,
3147    pub entries: Vec<TextureAtlasEntry>,
3148    pub cols: u32,
3149    pub rows: u32,
3150}
3151
3152impl TextureAtlas {
3153    pub fn new(id: u64, width: u32, height: u32, cols: u32, rows: u32) -> Self {
3154        Self { id, width, height, entries: Vec::new(), cols, rows }
3155    }
3156    pub fn add_entry(&mut self, id: u64, name: &str, col: u32, row: u32, frame_count: u32, fps: f32) {
3157        let uv_min = Vec2::new(col as f32 / self.cols as f32, row as f32 / self.rows as f32);
3158        let uv_max = Vec2::new((col + 1) as f32 / self.cols as f32, (row + 1) as f32 / self.rows as f32);
3159        self.entries.push(TextureAtlasEntry { id, name: name.to_string(), uv_min, uv_max, frame_count, fps });
3160    }
3161    pub fn find_by_name(&self, name: &str) -> Option<&TextureAtlasEntry> {
3162        self.entries.iter().find(|e| e.name == name)
3163    }
3164    pub fn cell_uv(&self, frame: u32) -> (Vec2, Vec2) {
3165        let col = frame % self.cols;
3166        let row = frame / self.cols;
3167        let min = Vec2::new(col as f32 / self.cols as f32, row as f32 / self.rows as f32);
3168        let max = Vec2::new((col + 1) as f32 / self.cols as f32, (row + 1) as f32 / self.rows as f32);
3169        (min, max)
3170    }
3171}
3172
3173// ============================================================
3174// PARTICLE EFFECT ASSET
3175// ============================================================
3176
3177#[derive(Clone, Debug)]
3178pub struct ParticleEffectAsset {
3179    pub id: u64,
3180    pub name: String,
3181    pub version: u32,
3182    pub system: ParticleSystem,
3183    pub atlas: Option<TextureAtlas>,
3184    pub tags: Vec<String>,
3185    pub author: String,
3186    pub description: String,
3187    pub thumbnail_id: Option<u64>,
3188    pub created_at: u64,
3189    pub modified_at: u64,
3190}
3191
3192impl ParticleEffectAsset {
3193    pub fn new(name: &str, system: ParticleSystem) -> Self {
3194        Self {
3195            id: 0,
3196            name: name.to_string(),
3197            version: 1,
3198            system,
3199            atlas: None,
3200            tags: Vec::new(),
3201            author: String::new(),
3202            description: String::new(),
3203            thumbnail_id: None,
3204            created_at: 0,
3205            modified_at: 0,
3206        }
3207    }
3208    pub fn add_tag(&mut self, tag: &str) {
3209        if !self.tags.iter().any(|t| t == tag) {
3210            self.tags.push(tag.to_string());
3211        }
3212    }
3213    pub fn remove_tag(&mut self, tag: &str) {
3214        self.tags.retain(|t| t != tag);
3215    }
3216    pub fn bump_version(&mut self) {
3217        self.version += 1;
3218    }
3219}
3220
3221// ============================================================
3222// MAIN EDITOR
3223// ============================================================
3224
3225#[derive(Clone, Debug)]
3226pub struct ParticleSystemEditor {
3227    pub active_asset: Option<ParticleEffectAsset>,
3228    pub undo_redo: UndoRedoStack,
3229    pub selection: SelectionState,
3230    pub preview: PreviewState,
3231    pub active_tab: EditorTab,
3232    pub emitter_panel: EmitterPanelState,
3233    pub preset_panel: PresetPanelState,
3234    pub statistics: StatisticsState,
3235    pub gpu_panel: GpuSettingsPanelState,
3236    pub curve_editor: CurveEditorState,
3237    pub color_picker: ColorPickerState,
3238    pub notifications: NotificationCenter,
3239    pub preset_manager: PresetManager,
3240    pub renderer: ParticleRenderer,
3241    pub sim_runner: SimulationRunner,
3242    pub inspector: EmitterInspectorState,
3243    pub shortcuts: Vec<KeyShortcut>,
3244    pub recent_files: RecentFiles,
3245    pub search_query: String,
3246    pub search_results: Vec<SearchResult>,
3247    pub next_id: u64,
3248    pub dirty: bool,
3249    pub current_file_path: Option<String>,
3250}
3251
3252impl ParticleSystemEditor {
3253    pub fn new() -> Self {
3254        Self {
3255            active_asset: None,
3256            undo_redo: UndoRedoStack::new(100),
3257            selection: SelectionState::new(),
3258            preview: PreviewState::new(),
3259            active_tab: EditorTab::Emitters,
3260            emitter_panel: EmitterPanelState::new(),
3261            preset_panel: PresetPanelState::new(),
3262            statistics: StatisticsState::new(),
3263            gpu_panel: GpuSettingsPanelState::new(),
3264            curve_editor: CurveEditorState::new(),
3265            color_picker: ColorPickerState::new(),
3266            notifications: NotificationCenter::new(),
3267            preset_manager: PresetManager::new(),
3268            renderer: ParticleRenderer::new(),
3269            sim_runner: SimulationRunner::new(),
3270            inspector: EmitterInspectorState::new(),
3271            shortcuts: default_shortcuts(),
3272            recent_files: RecentFiles::new(),
3273            search_query: String::new(),
3274            search_results: Vec::new(),
3275            next_id: 1,
3276            dirty: false,
3277            current_file_path: None,
3278        }
3279    }
3280
3281    pub fn new_system(&mut self, name: &str) {
3282        let sys = ParticleSystem::new(name);
3283        let asset = ParticleEffectAsset::new(name, sys);
3284        self.active_asset = Some(asset);
3285        self.undo_redo.clear();
3286        self.selection.deselect_all();
3287        self.dirty = false;
3288        self.current_file_path = None;
3289        self.notifications.info(&format!("Created new particle system: {}", name));
3290    }
3291
3292    pub fn load_preset(&mut self, preset: EffectPreset) {
3293        let sys = preset.create();
3294        let name = preset.name().to_string();
3295        let asset = ParticleEffectAsset::new(&name, sys);
3296        self.active_asset = Some(asset);
3297        self.undo_redo.clear();
3298        self.selection.deselect_all();
3299        self.dirty = true;
3300        self.preset_manager.mark_used(preset.name());
3301        self.notifications.success(&format!("Loaded preset: {}", name));
3302    }
3303
3304    pub fn add_emitter(&mut self, mut emitter: ParticleEmitter) {
3305        if let Some(asset) = &mut self.active_asset {
3306            let id = self.next_id;
3307            self.next_id += 1;
3308            emitter.id = id;
3309            let action = ParticleEditorAction::AddEmitter {
3310                system_id: asset.system.id,
3311                emitter: emitter.clone(),
3312            };
3313            asset.system.emitters.push(emitter);
3314            self.undo_redo.push(action);
3315            self.dirty = true;
3316            self.notifications.info("Emitter added");
3317        }
3318    }
3319
3320    pub fn remove_emitter(&mut self, emitter_id: u64) {
3321        if let Some(asset) = &mut self.active_asset {
3322            if let Some(pos) = asset.system.emitters.iter().position(|e| e.id == emitter_id) {
3323                let removed = asset.system.emitters.remove(pos);
3324                let action = ParticleEditorAction::RemoveEmitter {
3325                    system_id: asset.system.id,
3326                    emitter_id,
3327                    emitter: removed,
3328                };
3329                self.undo_redo.push(action);
3330                self.selection.selected_emitter_ids.remove(&emitter_id);
3331                if self.selection.focused_emitter_id == Some(emitter_id) {
3332                    self.selection.focused_emitter_id = None;
3333                }
3334                self.dirty = true;
3335                self.notifications.info("Emitter removed");
3336            }
3337        }
3338    }
3339
3340    pub fn duplicate_emitter(&mut self, emitter_id: u64) -> Option<u64> {
3341        if let Some(asset) = &mut self.active_asset {
3342            if let Some(src) = asset.system.emitters.iter().find(|e| e.id == emitter_id) {
3343                let mut new_emitter = src.clone();
3344                let new_id = self.next_id;
3345                self.next_id += 1;
3346                new_emitter.id = new_id;
3347                new_emitter.name = format!("{} (Copy)", new_emitter.name);
3348                let action = ParticleEditorAction::AddEmitter {
3349                    system_id: asset.system.id,
3350                    emitter: new_emitter.clone(),
3351                };
3352                asset.system.emitters.push(new_emitter);
3353                self.undo_redo.push(action);
3354                self.dirty = true;
3355                self.notifications.info("Emitter duplicated");
3356                return Some(new_id);
3357            }
3358        }
3359        None
3360    }
3361
3362    pub fn add_force_field(&mut self, kind: ForceFieldKind) {
3363        if let Some(asset) = &mut self.active_asset {
3364            let inner = kind.create_default();
3365            let mut ff = ForceField::new(kind.name(), inner);
3366            let id = self.next_id;
3367            self.next_id += 1;
3368            ff.id = id;
3369            let action = ParticleEditorAction::AddForceField {
3370                system_id: asset.system.id,
3371                field: ff.clone(),
3372            };
3373            asset.system.force_fields.push(ff);
3374            self.undo_redo.push(action);
3375            self.dirty = true;
3376            self.notifications.info(&format!("Added {} force field", kind.name()));
3377        }
3378    }
3379
3380    pub fn remove_force_field(&mut self, field_id: u64) {
3381        if let Some(asset) = &mut self.active_asset {
3382            if let Some(pos) = asset.system.force_fields.iter().position(|f| f.id == field_id) {
3383                let removed = asset.system.force_fields.remove(pos);
3384                let action = ParticleEditorAction::RemoveForceField {
3385                    system_id: asset.system.id,
3386                    field_id,
3387                    field: removed,
3388                };
3389                self.undo_redo.push(action);
3390                self.dirty = true;
3391                self.notifications.info("Force field removed");
3392            }
3393        }
3394    }
3395
3396    pub fn play(&mut self) {
3397        if let Some(asset) = &mut self.active_asset {
3398            asset.system.play_all();
3399            self.preview.is_playing = true;
3400            self.preview.is_paused = false;
3401        }
3402    }
3403
3404    pub fn stop(&mut self) {
3405        if let Some(asset) = &mut self.active_asset {
3406            asset.system.stop_all();
3407            self.preview.is_playing = false;
3408            self.preview.is_paused = false;
3409        }
3410    }
3411
3412    pub fn pause(&mut self) {
3413        if let Some(asset) = &mut self.active_asset {
3414            for e in &mut asset.system.emitters { e.pause(); }
3415            self.preview.is_paused = true;
3416        }
3417    }
3418
3419    pub fn reset(&mut self) {
3420        if let Some(asset) = &mut self.active_asset {
3421            asset.system.reset_all();
3422            self.preview.time = 0.0;
3423            self.sim_runner.reset();
3424        }
3425    }
3426
3427    pub fn update(&mut self, dt: f32) {
3428        if let Some(asset) = &mut self.active_asset {
3429            if self.preview.is_playing && !self.preview.is_paused {
3430                let scaled_dt = dt * self.preview.playback_speed;
3431                self.sim_runner.step(scaled_dt, &mut asset.system);
3432                self.preview.time += scaled_dt;
3433            }
3434            let total = asset.system.total_alive() as f32;
3435            self.statistics.push_sample(total, if dt > 0.0 { 1.0 / dt } else { 0.0 });
3436            let camera_pos = self.preview.camera_position;
3437            self.renderer.prepare_for_system(&asset.system, camera_pos);
3438        }
3439        self.notifications.update(dt);
3440    }
3441
3442    pub fn undo(&mut self) {
3443        if let Some(action) = self.undo_redo.pop_undo() {
3444            self.apply_undo(action);
3445            self.dirty = true;
3446        }
3447    }
3448
3449    pub fn redo(&mut self) {
3450        if let Some(action) = self.undo_redo.pop_redo() {
3451            self.apply_redo(action);
3452            self.dirty = true;
3453        }
3454    }
3455
3456    fn apply_undo(&mut self, action: ParticleEditorAction) {
3457        if let Some(asset) = &mut self.active_asset {
3458            match action {
3459                ParticleEditorAction::AddEmitter { emitter, .. } => {
3460                    asset.system.emitters.retain(|e| e.id != emitter.id);
3461                }
3462                ParticleEditorAction::RemoveEmitter { emitter, .. } => {
3463                    asset.system.emitters.push(emitter);
3464                }
3465                ParticleEditorAction::ModifyEmitter { emitter_id, before, .. } => {
3466                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3467                        *e = *before;
3468                    }
3469                }
3470                ParticleEditorAction::AddForceField { field, .. } => {
3471                    asset.system.force_fields.retain(|f| f.id != field.id);
3472                }
3473                ParticleEditorAction::RemoveForceField { field, .. } => {
3474                    asset.system.force_fields.push(field);
3475                }
3476                ParticleEditorAction::SetEmitterEnabled { emitter_id, old_state, .. } => {
3477                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3478                        e.enabled = old_state;
3479                    }
3480                }
3481                ParticleEditorAction::SetEmissionRate { emitter_id, old_rate, .. } => {
3482                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3483                        e.emission_rate = old_rate;
3484                    }
3485                }
3486                ParticleEditorAction::SetEmitterBlend { emitter_id, old_mode, .. } => {
3487                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3488                        e.blend_mode = old_mode;
3489                    }
3490                }
3491                ParticleEditorAction::RenameSystem { old_name, .. } => {
3492                    asset.system.name = old_name;
3493                }
3494                ParticleEditorAction::BatchDelete { emitters, .. } => {
3495                    asset.system.emitters.extend(emitters);
3496                }
3497                _ => {}
3498            }
3499        }
3500    }
3501
3502    fn apply_redo(&mut self, action: ParticleEditorAction) {
3503        if let Some(asset) = &mut self.active_asset {
3504            match action {
3505                ParticleEditorAction::AddEmitter { emitter, .. } => {
3506                    asset.system.emitters.push(emitter);
3507                }
3508                ParticleEditorAction::RemoveEmitter { emitter_id, .. } => {
3509                    asset.system.emitters.retain(|e| e.id != emitter_id);
3510                }
3511                ParticleEditorAction::ModifyEmitter { emitter_id, after, .. } => {
3512                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3513                        *e = *after;
3514                    }
3515                }
3516                ParticleEditorAction::AddForceField { field, .. } => {
3517                    asset.system.force_fields.push(field);
3518                }
3519                ParticleEditorAction::RemoveForceField { field_id, .. } => {
3520                    asset.system.force_fields.retain(|f| f.id != field_id);
3521                }
3522                ParticleEditorAction::SetEmitterEnabled { emitter_id, new_state, .. } => {
3523                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3524                        e.enabled = new_state;
3525                    }
3526                }
3527                ParticleEditorAction::SetEmissionRate { emitter_id, new_rate, .. } => {
3528                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3529                        e.emission_rate = new_rate;
3530                    }
3531                }
3532                ParticleEditorAction::SetEmitterBlend { emitter_id, new_mode, .. } => {
3533                    if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3534                        e.blend_mode = new_mode;
3535                    }
3536                }
3537                ParticleEditorAction::RenameSystem { new_name, .. } => {
3538                    asset.system.name = new_name;
3539                }
3540                ParticleEditorAction::BatchDelete { emitters, .. } => {
3541                    let ids: Vec<u64> = emitters.iter().map(|e| e.id).collect();
3542                    asset.system.emitters.retain(|e| !ids.contains(&e.id));
3543                }
3544                _ => {}
3545            }
3546        }
3547    }
3548
3549    pub fn search(&mut self, query: &str) {
3550        self.search_query = query.to_string();
3551        if let Some(asset) = &self.active_asset {
3552            self.search_results = search_system(&asset.system, query);
3553        } else {
3554            self.search_results.clear();
3555        }
3556    }
3557
3558    pub fn validate(&self) -> ValidationResult {
3559        if let Some(asset) = &self.active_asset {
3560            validate_system(&asset.system)
3561        } else {
3562            ValidationResult::new()
3563        }
3564    }
3565
3566    pub fn select_all_emitters(&mut self) {
3567        if let Some(asset) = &self.active_asset {
3568            for e in &asset.system.emitters {
3569                self.selection.selected_emitter_ids.insert(e.id);
3570            }
3571        }
3572    }
3573
3574    pub fn delete_selected(&mut self) {
3575        let ids: Vec<u64> = self.selection.selected_emitter_ids.iter().cloned().collect();
3576        if ids.is_empty() { return; }
3577        if let Some(asset) = &mut self.active_asset {
3578            let removed = batch_delete(&mut asset.system, &ids);
3579            let action = ParticleEditorAction::BatchDelete {
3580                system_id: asset.system.id,
3581                emitters: removed,
3582            };
3583            self.undo_redo.push(action);
3584            self.selection.deselect_all();
3585            self.dirty = true;
3586            self.notifications.info(&format!("Deleted {} emitters", ids.len()));
3587        }
3588    }
3589
3590    pub fn get_statistics_summary(&self) -> String {
3591        if let Some(asset) = &self.active_asset {
3592            let total = asset.system.total_alive();
3593            let spawned = asset.system.total_spawned();
3594            let emitters = asset.system.emitters.len();
3595            let ffs = asset.system.force_fields.len();
3596            let avg_fps = self.statistics.average_fps();
3597            format!(
3598                "Particles: {}/{} | Emitters: {} | Force Fields: {} | Avg FPS: {:.1} | Total Spawned: {}",
3599                total, asset.system.emitters.iter().map(|e| e.max_particles).sum::<u32>(),
3600                emitters, ffs, avg_fps, spawned
3601            )
3602        } else {
3603            "No active system".to_string()
3604        }
3605    }
3606
3607    pub fn rename_system(&mut self, new_name: &str) {
3608        if let Some(asset) = &mut self.active_asset {
3609            let old_name = asset.system.name.clone();
3610            let action = ParticleEditorAction::RenameSystem {
3611                system_id: asset.system.id,
3612                old_name: old_name.clone(),
3613                new_name: new_name.to_string(),
3614            };
3615            asset.system.name = new_name.to_string();
3616            asset.name = new_name.to_string();
3617            self.undo_redo.push(action);
3618            self.dirty = true;
3619        }
3620    }
3621
3622    pub fn set_emitter_enabled(&mut self, emitter_id: u64, enabled: bool) {
3623        if let Some(asset) = &mut self.active_asset {
3624            if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3625                let old_state = e.enabled;
3626                if old_state == enabled { return; }
3627                e.enabled = enabled;
3628                let action = ParticleEditorAction::SetEmitterEnabled {
3629                    system_id: asset.system.id,
3630                    emitter_id,
3631                    old_state,
3632                    new_state: enabled,
3633                };
3634                self.undo_redo.push(action);
3635                self.dirty = true;
3636            }
3637        }
3638    }
3639
3640    pub fn set_emission_rate(&mut self, emitter_id: u64, rate: f32) {
3641        if let Some(asset) = &mut self.active_asset {
3642            if let Some(e) = asset.system.emitters.iter_mut().find(|e| e.id == emitter_id) {
3643                let old_rate = e.emission_rate;
3644                e.emission_rate = rate.max(0.0);
3645                let action = ParticleEditorAction::SetEmissionRate {
3646                    system_id: asset.system.id,
3647                    emitter_id,
3648                    old_rate,
3649                    new_rate: rate,
3650                };
3651                self.undo_redo.push(action);
3652                self.dirty = true;
3653            }
3654        }
3655    }
3656
3657    pub fn process_command(&mut self, cmd: EditorCommand) {
3658        match cmd {
3659            EditorCommand::Undo => self.undo(),
3660            EditorCommand::Redo => self.redo(),
3661            EditorCommand::Play => self.play(),
3662            EditorCommand::Stop => self.stop(),
3663            EditorCommand::Pause => self.pause(),
3664            EditorCommand::Reset => self.reset(),
3665            EditorCommand::Delete => self.delete_selected(),
3666            EditorCommand::SelectAll => self.select_all_emitters(),
3667            EditorCommand::DeselectAll => self.selection.deselect_all(),
3668            EditorCommand::ToggleGrid => { self.preview.show_grid = !self.preview.show_grid; }
3669            EditorCommand::ToggleBounds => { self.preview.show_bounds = !self.preview.show_bounds; }
3670            EditorCommand::ToggleStatistics => { self.preview.show_statistics = !self.preview.show_statistics; }
3671            EditorCommand::ToggleForceFields => { self.preview.show_force_fields = !self.preview.show_force_fields; }
3672            EditorCommand::Duplicate => {
3673                let ids: Vec<u64> = self.selection.selected_emitter_ids.iter().cloned().collect();
3674                for id in ids {
3675                    self.duplicate_emitter(id);
3676                }
3677            }
3678            EditorCommand::AddEmitter => {
3679                let e = ParticleEmitter::new("New Emitter");
3680                self.add_emitter(e);
3681            }
3682            EditorCommand::OpenPresets => { self.active_tab = EditorTab::Presets; }
3683            EditorCommand::ToggleCurveEditor => {
3684                self.active_tab = if self.active_tab == EditorTab::CurveEditor {
3685                    EditorTab::Emitters
3686                } else {
3687                    EditorTab::CurveEditor
3688                };
3689            }
3690            _ => {}
3691        }
3692    }
3693}
3694
3695// ============================================================
3696// PARTICLE EDITOR APP
3697// ============================================================
3698
3699#[derive(Clone, Debug)]
3700pub struct ParticleEditorApp {
3701    pub editor: ParticleSystemEditor,
3702    pub window_title: String,
3703    pub window_width: u32,
3704    pub window_height: u32,
3705    pub panel_left_width: f32,
3706    pub panel_right_width: f32,
3707    pub show_left_panel: bool,
3708    pub show_right_panel: bool,
3709    pub show_bottom_panel: bool,
3710    pub bottom_panel_height: f32,
3711    pub theme: EditorTheme,
3712    pub fps: f32,
3713    pub frame_count: u64,
3714}
3715
3716#[derive(Clone, Copy, Debug, PartialEq)]
3717pub enum EditorTheme { Dark, Light, HighContrast }
3718
3719impl ParticleEditorApp {
3720    pub fn new() -> Self {
3721        let mut app = Self {
3722            editor: ParticleSystemEditor::new(),
3723            window_title: "Particle System Editor".to_string(),
3724            window_width: 1920,
3725            window_height: 1080,
3726            panel_left_width: 320.0,
3727            panel_right_width: 380.0,
3728            show_left_panel: true,
3729            show_right_panel: true,
3730            show_bottom_panel: true,
3731            bottom_panel_height: 200.0,
3732            theme: EditorTheme::Dark,
3733            fps: 0.0,
3734            frame_count: 0,
3735        };
3736        app.editor.new_system("Untitled Effect");
3737        app
3738    }
3739
3740    pub fn update(&mut self, dt: f32) {
3741        self.fps = if dt > 0.0 { 1.0 / dt } else { 0.0 };
3742        self.frame_count += 1;
3743        self.editor.update(dt);
3744    }
3745
3746    pub fn title(&self) -> String {
3747        let dirty = if self.editor.dirty { "*" } else { "" };
3748        let name = self.editor.active_asset.as_ref().map(|a| a.name.as_str()).unwrap_or("No System");
3749        format!("{}{} - {}", dirty, name, self.window_title)
3750    }
3751
3752    pub fn resize(&mut self, width: u32, height: u32) {
3753        self.window_width = width;
3754        self.window_height = height;
3755    }
3756
3757    pub fn preview_viewport_rect(&self) -> (f32, f32, f32, f32) {
3758        let left = if self.show_left_panel { self.panel_left_width } else { 0.0 };
3759        let right = if self.show_right_panel { self.panel_right_width } else { 0.0 };
3760        let bottom = if self.show_bottom_panel { self.bottom_panel_height } else { 0.0 };
3761        let w = self.window_width as f32 - left - right;
3762        let h = self.window_height as f32 - 40.0 - bottom;
3763        (left, 40.0, w, h)
3764    }
3765}
3766
3767// ============================================================
3768// CYLINDER EMITTER SHAPE (additional variant helper)
3769// ============================================================
3770
3771impl EmitterShape {
3772    pub fn cylinder(radius: f32, height: f32) -> Self {
3773        EmitterShape::Cone { radius, angle_deg: 0.0, length: height }
3774    }
3775}
3776
3777// ============================================================
3778// SELF TESTS
3779// ============================================================
3780
3781pub fn test_curve_linear() -> bool {
3782    let c = FloatCurve::linear(0.0, 1.0);
3783    let mid = c.evaluate(0.5);
3784    (mid - 0.5).abs() < 0.001
3785}
3786
3787pub fn test_curve_constant() -> bool {
3788    let c = FloatCurve::constant(3.14);
3789    (c.evaluate(0.0) - 3.14).abs() < 0.001
3790        && (c.evaluate(0.5) - 3.14).abs() < 0.001
3791        && (c.evaluate(1.0) - 3.14).abs() < 0.001
3792}
3793
3794pub fn test_gradient_lerp() -> bool {
3795    let mut g = ColorGradient::new();
3796    g.add_key(GradientKey::new(0.0, Vec4::new(0.0, 0.0, 0.0, 1.0)));
3797    g.add_key(GradientKey::new(1.0, Vec4::new(1.0, 1.0, 1.0, 1.0)));
3798    let mid = g.evaluate(0.5);
3799    (mid.x - 0.5).abs() < 0.001 && (mid.y - 0.5).abs() < 0.001
3800}
3801
3802pub fn test_spatial_hash() -> bool {
3803    let mut sh = SpatialHash::new(1.0);
3804    sh.insert(Vec3::new(0.5, 0.5, 0.5), 0);
3805    sh.insert(Vec3::new(5.0, 5.0, 5.0), 1);
3806    let near = sh.query_radius(Vec3::new(0.5, 0.5, 0.5), 0.1);
3807    let far = sh.query_radius(Vec3::new(10.0, 10.0, 10.0), 0.1);
3808    near.contains(&0) && far.is_empty()
3809}
3810
3811pub fn test_verlet() -> bool {
3812    let mut p = Particle::new(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), 5.0, Vec4::ONE, 0.1);
3813    p.prev_position = Vec3::new(-0.016, 0.0, 0.0); // set up for verlet
3814    p.integrate_verlet(0.016);
3815    p.position.x > 0.0
3816}
3817
3818pub fn test_lorentz() -> bool {
3819    // F = q(v x B), v = (1,0,0), B = (0,1,0), q=1 => F = (0,0,-1) * ... check sign
3820    let mf = MagneticForce::new(Vec3::Y, 1.0);
3821    let f = mf.apply(Vec3::X);
3822    // v x B = X x Y = Z (right-hand), so F should be (0,0,1)
3823    (f.z - 1.0).abs() < 0.001
3824}
3825
3826pub fn test_perlin() -> bool {
3827    let v1 = perlin3(Vec3::new(0.1, 0.2, 0.3));
3828    let v2 = perlin3(Vec3::new(0.1, 0.2, 0.3));
3829    let v3 = perlin3(Vec3::new(5.1, 2.2, 3.3));
3830    v1 == v2 && (v1 - v3).abs() > 0.0001
3831}
3832
3833pub fn test_rng_range() -> bool {
3834    let mut rng = SimpleRng::new(42);
3835    for _ in 0..1000 {
3836        let v = rng.next_f32_range(-1.0, 1.0);
3837        if v < -1.0 || v > 1.0 { return false; }
3838    }
3839    true
3840}
3841
3842pub fn test_color_picker_hex() -> bool {
3843    let color = Vec4::new(1.0, 0.0, 0.0, 1.0);
3844    let hex = ColorPickerState::color_to_hex(color);
3845    let back = ColorPickerState::hex_to_color(&hex);
3846    if let Some(c) = back {
3847        (c.x - 1.0).abs() < 0.01 && c.y.abs() < 0.01 && c.z.abs() < 0.01
3848    } else {
3849        false
3850    }
3851}
3852
3853pub fn test_hsv_roundtrip() -> bool {
3854    let original = Vec3::new(0.8, 0.6, 0.9);
3855    let hsv = ColorPickerState::rgb_to_hsv(original);
3856    let back = ColorPickerState::hsv_to_rgb(hsv);
3857    (back.x - original.x).abs() < 0.01
3858        && (back.y - original.y).abs() < 0.01
3859        && (back.z - original.z).abs() < 0.01
3860}
3861
3862pub fn test_undo_redo() -> bool {
3863    let mut stack = UndoRedoStack::new(10);
3864    assert!(!stack.can_undo());
3865    let e = ParticleEmitter::new("Test");
3866    stack.push(ParticleEditorAction::AddEmitter { system_id: 1, emitter: e });
3867    assert!(stack.can_undo());
3868    assert!(!stack.can_redo());
3869    stack.pop_undo();
3870    assert!(!stack.can_undo());
3871    assert!(stack.can_redo());
3872    stack.pop_redo();
3873    assert!(stack.can_undo());
3874    true
3875}
3876
3877pub fn test_lod_system() -> bool {
3878    let lod = LodSystem::default_levels();
3879    let l1 = lod.get_level(5.0);
3880    let l2 = lod.get_level(500.0);
3881    l1.quality == SimQuality::Full && l2.quality == SimQuality::Culled
3882}
3883
3884pub fn test_emitter_shape_sphere() -> bool {
3885    let shape = EmitterShape::Sphere { radius: 2.0, emit_from_shell: false };
3886    let mut rng = SimpleRng::new(99);
3887    for _ in 0..100 {
3888        let (pos, _) = shape.sample_position(&mut rng);
3889        if pos.length() > 2.001 { return false; }
3890    }
3891    true
3892}
3893
3894pub fn test_preset_creates_emitters() -> bool {
3895    let sys = preset_fire();
3896    !sys.emitters.is_empty()
3897}
3898
3899pub fn test_particle_age() -> bool {
3900    let mut p = Particle::new(Vec3::ZERO, Vec3::ZERO, 2.0, Vec4::ONE, 0.1);
3901    assert!(!p.is_dead());
3902    p.age = 2.0;
3903    p.is_dead()
3904}
3905
3906pub fn test_force_field_magnetic_lorentz() -> bool {
3907    // Lorentz: F = q * (v x B)
3908    // v = (1,0,0), B = (0,0,1), q=2 => v x B = (0,0,0)+(0*1-0*0, 0*0-1*1, 1*0-0*0) = (0,-1,0)
3909    // F = 2 * (0,-1,0) = (0,-2,0)
3910    let mf = MagneticForce::new(Vec3::Z, 2.0);
3911    let f = mf.apply(Vec3::X);
3912    (f.y - (-2.0)).abs() < 0.001
3913}
3914
3915pub fn test_vortex_force() -> bool {
3916    let vf = VortexForceField::new(Vec3::ZERO, 1.0);
3917    let f = vf.apply(Vec3::new(1.0, 0.0, 0.0));
3918    // For pos=(1,0,0), axis=Y, radial=(1,0,0), tangent = Y x (1,0,0) = (0,0,-1)
3919    f.length() > 0.0
3920}
3921
3922pub fn test_texture_atlas() -> bool {
3923    let mut atlas = TextureAtlas::new(1, 512, 512, 4, 4);
3924    atlas.add_entry(1, "fire_frame", 0, 0, 4, 24.0);
3925    let entry = atlas.find_by_name("fire_frame");
3926    entry.is_some()
3927}
3928
3929pub fn run_all_tests() -> (u32, u32) {
3930    let tests: &[(&str, fn() -> bool)] = &[
3931        ("curve_linear", test_curve_linear),
3932        ("curve_constant", test_curve_constant),
3933        ("gradient_lerp", test_gradient_lerp),
3934        ("spatial_hash", test_spatial_hash),
3935        ("verlet", test_verlet),
3936        ("lorentz", test_lorentz),
3937        ("magnetic_lorentz", test_force_field_magnetic_lorentz),
3938        ("perlin", test_perlin),
3939        ("rng_range", test_rng_range),
3940        ("color_hex", test_color_picker_hex),
3941        ("hsv_roundtrip", test_hsv_roundtrip),
3942        ("undo_redo", test_undo_redo),
3943        ("lod_system", test_lod_system),
3944        ("sphere_shape", test_emitter_shape_sphere),
3945        ("preset_creates_emitters", test_preset_creates_emitters),
3946        ("particle_age", test_particle_age),
3947        ("vortex_force", test_vortex_force),
3948        ("texture_atlas", test_texture_atlas),
3949    ];
3950    let mut passed = 0u32;
3951    let mut failed = 0u32;
3952    for (name, test) in tests {
3953        if test() { passed += 1; } else { failed += 1; }
3954    }
3955    (passed, failed)
3956}
3957
3958// ============================================================
3959// ADDITIONAL PARTICLE SYSTEM UTILITIES
3960// ============================================================
3961
3962pub fn calculate_particle_memory(emitter: &ParticleEmitter) -> usize {
3963    let particle_size = std::mem::size_of::<Particle>();
3964    particle_size * emitter.max_particles as usize
3965}
3966
3967pub fn calculate_system_memory(system: &ParticleSystem) -> usize {
3968    system.emitters.iter().map(calculate_particle_memory).sum()
3969}
3970
3971pub fn estimate_gpu_memory(params: &GpuParticleParams) -> u64 {
3972    params.buffer_bytes() * 3 // triple buffer estimate
3973}
3974
3975pub fn get_all_emitter_names(system: &ParticleSystem) -> Vec<String> {
3976    system.emitters.iter().map(|e| e.name.clone()).collect()
3977}
3978
3979pub fn find_emitter_by_name<'a>(system: &'a ParticleSystem, name: &str) -> Option<&'a ParticleEmitter> {
3980    system.emitters.iter().find(|e| e.name == name)
3981}
3982
3983pub fn find_emitter_by_name_mut<'a>(system: &'a mut ParticleSystem, name: &str) -> Option<&'a mut ParticleEmitter> {
3984    system.emitters.iter_mut().find(|e| e.name == name)
3985}
3986
3987pub fn emitter_bounds(emitter: &ParticleEmitter) -> Option<(Vec3, Vec3)> {
3988    if emitter.particles.is_empty() { return None; }
3989    let mut min = Vec3::splat(f32::INFINITY);
3990    let mut max = Vec3::splat(f32::NEG_INFINITY);
3991    for p in &emitter.particles {
3992        min = min.min(p.position);
3993        max = max.max(p.position);
3994    }
3995    Some((min, max))
3996}
3997
3998pub fn system_bounds(system: &ParticleSystem) -> Option<(Vec3, Vec3)> {
3999    let mut min = Vec3::splat(f32::INFINITY);
4000    let mut max = Vec3::splat(f32::NEG_INFINITY);
4001    let mut any = false;
4002    for e in &system.emitters {
4003        if let Some((emin, emax)) = emitter_bounds(e) {
4004            min = min.min(emin);
4005            max = max.max(emax);
4006            any = true;
4007        }
4008    }
4009    if any { Some((min, max)) } else { None }
4010}
4011
4012pub fn sort_particles_by_distance(particles: &mut Vec<Particle>, camera_pos: Vec3) {
4013    particles.sort_by(|a, b| {
4014        let da = (a.position - camera_pos).length_squared();
4015        let db = (b.position - camera_pos).length_squared();
4016        db.partial_cmp(&da).unwrap()
4017    });
4018}
4019
4020pub fn count_alive_particles(system: &ParticleSystem) -> u32 {
4021    system.emitters.iter().map(|e| e.particles.iter().filter(|p| p.alive).count() as u32).sum()
4022}
4023
4024pub fn scale_system(system: &mut ParticleSystem, scale: f32) {
4025    for e in &mut system.emitters {
4026        e.position *= scale;
4027        for p in &mut e.particles {
4028            p.position *= scale;
4029            p.velocity *= scale;
4030            p.size *= scale;
4031        }
4032    }
4033}
4034
4035pub fn translate_system(system: &mut ParticleSystem, offset: Vec3) {
4036    for e in &mut system.emitters {
4037        e.position += offset;
4038        for p in &mut e.particles {
4039            p.position += offset;
4040        }
4041    }
4042}
4043
4044// ============================================================
4045// COMPLEX CURVE OPERATIONS
4046// ============================================================
4047
4048pub fn curve_multiply(a: &FloatCurve, b: &FloatCurve, samples: usize) -> FloatCurve {
4049    let mut result = FloatCurve::new();
4050    for i in 0..=samples {
4051        let t = i as f32 / samples as f32;
4052        let v = a.evaluate(t) * b.evaluate(t);
4053        result.add_key(CurveKey::new(t, v));
4054    }
4055    result
4056}
4057
4058pub fn curve_add(a: &FloatCurve, b: &FloatCurve, samples: usize) -> FloatCurve {
4059    let mut result = FloatCurve::new();
4060    for i in 0..=samples {
4061        let t = i as f32 / samples as f32;
4062        let v = a.evaluate(t) + b.evaluate(t);
4063        result.add_key(CurveKey::new(t, v));
4064    }
4065    result
4066}
4067
4068pub fn curve_inverse(c: &FloatCurve, samples: usize) -> FloatCurve {
4069    let mut result = FloatCurve::new();
4070    for i in 0..=samples {
4071        let t = i as f32 / samples as f32;
4072        let v = 1.0 - c.evaluate(t);
4073        result.add_key(CurveKey::new(t, v));
4074    }
4075    result
4076}
4077
4078pub fn curve_normalize(c: &FloatCurve, samples: usize) -> FloatCurve {
4079    let vals: Vec<f32> = (0..=samples).map(|i| c.evaluate(i as f32 / samples as f32)).collect();
4080    let min = vals.iter().cloned().fold(f32::INFINITY, f32::min);
4081    let max = vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4082    let range = (max - min).max(0.0001);
4083    let mut result = FloatCurve::new();
4084    for (i, v) in vals.iter().enumerate() {
4085        let t = i as f32 / samples as f32;
4086        result.add_key(CurveKey::new(t, (v - min) / range));
4087    }
4088    result
4089}
4090
4091// ============================================================
4092// GRADIENT OPERATIONS
4093// ============================================================
4094
4095pub fn gradient_multiply(a: &ColorGradient, b: &ColorGradient, samples: usize) -> ColorGradient {
4096    let mut result = ColorGradient::new();
4097    for i in 0..=samples {
4098        let t = i as f32 / samples as f32;
4099        let ca = a.evaluate(t);
4100        let cb = b.evaluate(t);
4101        result.add_key(GradientKey::new(t, ca * cb));
4102    }
4103    result
4104}
4105
4106pub fn gradient_overlay(base: &ColorGradient, overlay: &ColorGradient, alpha: f32, samples: usize) -> ColorGradient {
4107    let mut result = ColorGradient::new();
4108    for i in 0..=samples {
4109        let t = i as f32 / samples as f32;
4110        let cb = base.evaluate(t);
4111        let co = overlay.evaluate(t);
4112        result.add_key(GradientKey::new(t, cb.lerp(co, alpha)));
4113    }
4114    result
4115}
4116
4117// ============================================================
4118// PARTICLE SYSTEM BUILDER
4119// ============================================================
4120
4121#[derive(Clone, Debug)]
4122pub struct ParticleSystemBuilder {
4123    system: ParticleSystem,
4124    next_id: u64,
4125}
4126
4127impl ParticleSystemBuilder {
4128    pub fn new(name: &str) -> Self {
4129        Self { system: ParticleSystem::new(name), next_id: 1 }
4130    }
4131    pub fn with_emitter(mut self, mut e: ParticleEmitter) -> Self {
4132        e.id = self.next_id;
4133        self.next_id += 1;
4134        self.system.emitters.push(e);
4135        self
4136    }
4137    pub fn with_force_field(mut self, mut ff: ForceField) -> Self {
4138        ff.id = self.next_id;
4139        self.next_id += 1;
4140        self.system.force_fields.push(ff);
4141        self
4142    }
4143    pub fn with_time_scale(mut self, scale: f32) -> Self {
4144        self.system.time_scale = scale;
4145        self
4146    }
4147    pub fn at_position(mut self, pos: Vec3) -> Self {
4148        self.system.world_position = pos;
4149        self
4150    }
4151    pub fn build(self) -> ParticleSystem { self.system }
4152}
4153
4154// ============================================================
4155// EMITTER BUILDER
4156// ============================================================
4157
4158#[derive(Clone, Debug)]
4159pub struct EmitterBuilder { emitter: ParticleEmitter }
4160
4161impl EmitterBuilder {
4162    pub fn new(name: &str) -> Self { Self { emitter: ParticleEmitter::new(name) } }
4163    pub fn shape(mut self, s: EmitterShape) -> Self { self.emitter.shape = s; self }
4164    pub fn emission_rate(mut self, r: f32) -> Self { self.emitter.emission_rate = r; self }
4165    pub fn max_particles(mut self, n: u32) -> Self { self.emitter.max_particles = n; self }
4166    pub fn lifetime(mut self, min: f32, max: f32) -> Self {
4167        self.emitter.lifetime_module = LifetimeModule::new(min, max); self
4168    }
4169    pub fn speed(mut self, min: f32, max: f32) -> Self {
4170        self.emitter.velocity_module = VelocityModule::new(min, max); self
4171    }
4172    pub fn size(mut self, min: f32, max: f32) -> Self {
4173        self.emitter.size_module = SizeModule::new(min, max); self
4174    }
4175    pub fn blend(mut self, b: ParticleBlendMode) -> Self { self.emitter.blend_mode = b; self }
4176    pub fn render_mode(mut self, r: ParticleRenderMode) -> Self { self.emitter.render_mode = r; self }
4177    pub fn color_gradient(mut self, g: ColorGradient) -> Self {
4178        self.emitter.color_module.color_over_lifetime = g; self
4179    }
4180    pub fn with_noise(mut self, freq: f32, amp: f32) -> Self {
4181        self.emitter.noise_module.enabled = true;
4182        self.emitter.noise_module.frequency = freq;
4183        self.emitter.noise_module.amplitude = amp;
4184        self
4185    }
4186    pub fn with_gravity(mut self, mult: f32) -> Self {
4187        self.emitter.gravity_module.gravity_multiplier = mult; self
4188    }
4189    pub fn with_collision(mut self) -> Self {
4190        self.emitter.collision_module.enabled = true; self
4191    }
4192    pub fn looping(mut self, l: bool) -> Self { self.emitter.looping = l; self }
4193    pub fn burst(mut self, time: f32, count: u32) -> Self {
4194        self.emitter.emission_bursts.push(EmissionBurst::new(time, count)); self
4195    }
4196    pub fn position(mut self, p: Vec3) -> Self { self.emitter.position = p; self }
4197    pub fn build(self) -> ParticleEmitter { self.emitter }
4198}
4199
4200// ============================================================
4201// EXTENDED FORCE FIELD PRESETS
4202// ============================================================
4203
4204pub fn make_wind_field(dir: Vec3, speed: f32) -> ForceField {
4205    ForceField::new("Wind", ForceFieldKindInner::Wind(WindForce::new(dir, speed)))
4206}
4207
4208pub fn make_gravity_well(center: Vec3, strength: f32) -> ForceField {
4209    ForceField::new("Gravity Well", ForceFieldKindInner::GravityPoint(GravityPointForce::new(center, strength)))
4210}
4211
4212pub fn make_repulsor(center: Vec3, strength: f32) -> ForceField {
4213    let mut f = GravityPointForce::new(center, strength);
4214    f.gravity_type = GravityType::Repel;
4215    ForceField::new("Repulsor", ForceFieldKindInner::GravityPoint(f))
4216}
4217
4218pub fn make_drag_field(coeff: f32) -> ForceField {
4219    ForceField::new("Drag", ForceFieldKindInner::Drag(DragForce::new(coeff)))
4220}
4221
4222pub fn make_turbulence_field(freq: f32, amp: f32) -> ForceField {
4223    ForceField::new("Turbulence", ForceFieldKindInner::Turbulence(TurbulenceForce::new(freq, amp)))
4224}
4225
4226pub fn make_magnetic_field(b: Vec3, charge: f32) -> ForceField {
4227    ForceField::new("Magnetic", ForceFieldKindInner::Magnetic(MagneticForce::new(b, charge)))
4228}
4229
4230// ============================================================
4231// PARTICLE STATISTICS HELPERS
4232// ============================================================
4233
4234#[derive(Clone, Debug, Default)]
4235pub struct SystemStatisticsSnapshot {
4236    pub total_alive: u32,
4237    pub total_max: u32,
4238    pub total_spawned: u64,
4239    pub emitter_count: u32,
4240    pub active_emitter_count: u32,
4241    pub force_field_count: u32,
4242    pub estimated_memory_bytes: usize,
4243}
4244
4245pub fn snapshot_statistics(system: &ParticleSystem) -> SystemStatisticsSnapshot {
4246    let mut snap = SystemStatisticsSnapshot::default();
4247    snap.emitter_count = system.emitters.len() as u32;
4248    snap.force_field_count = system.force_fields.len() as u32;
4249    for e in &system.emitters {
4250        snap.total_alive += e.statistics.alive_count;
4251        snap.total_max += e.max_particles;
4252        snap.total_spawned += e.statistics.total_spawned;
4253        if e.enabled { snap.active_emitter_count += 1; }
4254        snap.estimated_memory_bytes += calculate_particle_memory(e);
4255    }
4256    snap
4257}
4258
4259// ============================================================
4260// SERIALIZATION HELPERS (stubs - no external serde dep)
4261// ============================================================
4262
4263pub struct ParticleSystemSerializer;
4264
4265impl ParticleSystemSerializer {
4266    pub fn to_json_string(system: &ParticleSystem) -> String {
4267        let snap = snapshot_statistics(system);
4268        format!(
4269            r#"{{"name":"{}","emitters":{},"force_fields":{},"total_max_particles":{}}}"#,
4270            system.name,
4271            system.emitters.len(),
4272            system.force_fields.len(),
4273            snap.total_max
4274        )
4275    }
4276    pub fn emitter_to_json(e: &ParticleEmitter) -> String {
4277        format!(
4278            r#"{{"name":"{}","id":{},"enabled":{},"emission_rate":{},"max_particles":{},"lifetime_min":{},"lifetime_max":{}}}"#,
4279            e.name, e.id, e.enabled, e.emission_rate, e.max_particles,
4280            e.lifetime_module.min_lifetime, e.lifetime_module.max_lifetime
4281        )
4282    }
4283}
4284
4285// ============================================================
4286// PROFILING HELPERS
4287// ============================================================
4288
4289#[derive(Clone, Debug, Default)]
4290pub struct ParticleProfiler {
4291    pub update_times: VecDeque<f64>,
4292    pub spawn_times: VecDeque<f64>,
4293    pub render_times: VecDeque<f64>,
4294    pub history_size: usize,
4295}
4296
4297impl ParticleProfiler {
4298    pub fn new(history: usize) -> Self {
4299        Self { history_size: history, ..Default::default() }
4300    }
4301    pub fn push_update(&mut self, ms: f64) {
4302        self.update_times.push_back(ms);
4303        while self.update_times.len() > self.history_size { self.update_times.pop_front(); }
4304    }
4305    pub fn push_spawn(&mut self, ms: f64) {
4306        self.spawn_times.push_back(ms);
4307        while self.spawn_times.len() > self.history_size { self.spawn_times.pop_front(); }
4308    }
4309    pub fn push_render(&mut self, ms: f64) {
4310        self.render_times.push_back(ms);
4311        while self.render_times.len() > self.history_size { self.render_times.pop_front(); }
4312    }
4313    pub fn avg_update(&self) -> f64 {
4314        if self.update_times.is_empty() { return 0.0; }
4315        self.update_times.iter().sum::<f64>() / self.update_times.len() as f64
4316    }
4317    pub fn avg_spawn(&self) -> f64 {
4318        if self.spawn_times.is_empty() { return 0.0; }
4319        self.spawn_times.iter().sum::<f64>() / self.spawn_times.len() as f64
4320    }
4321    pub fn avg_render(&self) -> f64 {
4322        if self.render_times.is_empty() { return 0.0; }
4323        self.render_times.iter().sum::<f64>() / self.render_times.len() as f64
4324    }
4325}
4326
4327// ============================================================
4328// NOISE UTILITIES
4329// ============================================================
4330
4331pub fn value_noise_1d(x: f32) -> f32 {
4332    let ix = x.floor() as i32;
4333    let fx = x - x.floor();
4334    let a = PERM[(ix & 255) as usize] as f32 / 255.0;
4335    let b = PERM[((ix + 1) & 255) as usize] as f32 / 255.0;
4336    let t = fade(fx);
4337    lerp_f(a, b, t)
4338}
4339
4340pub fn value_noise_2d(p: Vec2) -> f32 {
4341    let ix = p.x.floor() as i32;
4342    let iy = p.y.floor() as i32;
4343    let fx = p.x - p.x.floor();
4344    let fy = p.y - p.y.floor();
4345    let aa = PERM[((PERM[(ix & 255) as usize] as i32 + iy) & 255) as usize] as f32 / 255.0;
4346    let ba = PERM[((PERM[((ix + 1) & 255) as usize] as i32 + iy) & 255) as usize] as f32 / 255.0;
4347    let ab = PERM[((PERM[(ix & 255) as usize] as i32 + iy + 1) & 255) as usize] as f32 / 255.0;
4348    let bb = PERM[((PERM[((ix + 1) & 255) as usize] as i32 + iy + 1) & 255) as usize] as f32 / 255.0;
4349    let ux = fade(fx); let uy = fade(fy);
4350    lerp_f(lerp_f(aa, ba, ux), lerp_f(ab, bb, ux), uy)
4351}
4352
4353pub fn domain_warp(p: Vec3, warp_strength: f32, time: f32) -> Vec3 {
4354    let wx = fbm_noise(p + Vec3::new(1.7, 9.2, 2.3) + Vec3::splat(time * 0.1), 2, 0.5, 2.0);
4355    let wy = fbm_noise(p + Vec3::new(8.3, 2.8, 7.1) + Vec3::splat(time * 0.1), 2, 0.5, 2.0);
4356    let wz = fbm_noise(p + Vec3::new(3.1, 5.7, 1.4) + Vec3::splat(time * 0.1), 2, 0.5, 2.0);
4357    p + Vec3::new(wx, wy, wz) * warp_strength
4358}
4359
4360// ============================================================
4361// ADDITIONAL EMITTER OPERATIONS
4362// ============================================================
4363
4364pub fn reset_emitter_stats(e: &mut ParticleEmitter) {
4365    e.statistics = EmitterStatistics::default();
4366}
4367
4368pub fn emitter_utilization(e: &ParticleEmitter) -> f32 {
4369    if e.max_particles == 0 { return 0.0; }
4370    e.statistics.alive_count as f32 / e.max_particles as f32
4371}
4372
4373pub fn prewarm_emitter(e: &mut ParticleEmitter, duration: f32, dt: f32, force_fields: &[ForceField]) {
4374    e.play();
4375    let mut t = 0.0_f32;
4376    while t < duration {
4377        e.update(dt, force_fields);
4378        t += dt;
4379    }
4380}
4381
4382pub fn clone_emitter_with_offset(src: &ParticleEmitter, offset: Vec3, new_id: u64) -> ParticleEmitter {
4383    let mut e = src.clone();
4384    e.id = new_id;
4385    e.position += offset;
4386    e.particles.clear();
4387    e.statistics = EmitterStatistics::default();
4388    e
4389}
4390
4391// ============================================================
4392// PARTICLE EFFECTS COMBINATORS
4393// ============================================================
4394
4395pub fn combine_systems(name: &str, systems: Vec<ParticleSystem>) -> ParticleSystem {
4396    let mut combined = ParticleSystem::new(name);
4397    let mut next_id = 1u64;
4398    for sys in systems {
4399        for mut e in sys.emitters {
4400            e.id = next_id;
4401            next_id += 1;
4402            combined.emitters.push(e);
4403        }
4404        for mut ff in sys.force_fields {
4405            ff.id = next_id;
4406            next_id += 1;
4407            combined.force_fields.push(ff);
4408        }
4409    }
4410    combined
4411}
4412
4413pub fn extract_emitter(system: &mut ParticleSystem, id: u64) -> Option<ParticleEmitter> {
4414    if let Some(pos) = system.emitters.iter().position(|e| e.id == id) {
4415        Some(system.emitters.remove(pos))
4416    } else {
4417        None
4418    }
4419}
4420
4421// ============================================================
4422// CAMERA UTILITIES
4423// ============================================================
4424
4425pub fn compute_view_matrix(position: Vec3, target: Vec3, up: Vec3) -> Mat4 {
4426    Mat4::look_at_rh(position, target, up)
4427}
4428
4429pub fn compute_projection_matrix(fov_deg: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
4430    Mat4::perspective_rh(fov_deg.to_radians(), aspect, near, far)
4431}
4432
4433pub fn project_point(pos: Vec3, view: Mat4, proj: Mat4, viewport: Vec2) -> Vec2 {
4434    let clip = proj * view * pos.extend(1.0);
4435    if clip.w.abs() < 0.0001 { return Vec2::ZERO; }
4436    let ndc = Vec2::new(clip.x / clip.w, clip.y / clip.w);
4437    Vec2::new(
4438        (ndc.x + 1.0) * 0.5 * viewport.x,
4439        (1.0 - ndc.y) * 0.5 * viewport.y,
4440    )
4441}
4442
4443// ============================================================
4444// EXTRA PRESETS AND BUILDERS
4445// ============================================================
4446
4447pub fn build_campfire() -> ParticleSystem {
4448    let fire = EmitterBuilder::new("Fire Core")
4449        .shape(EmitterShape::Disk { radius: 0.3 })
4450        .emission_rate(60.0)
4451        .max_particles(300)
4452        .lifetime(0.4, 1.2)
4453        .speed(1.5, 3.0)
4454        .size(0.1, 0.5)
4455        .blend(ParticleBlendMode::Additive)
4456        .with_noise(1.5, 0.4)
4457        .with_gravity(-0.3)
4458        .color_gradient(fire_gradient())
4459        .build();
4460
4461    let smoke = EmitterBuilder::new("Smoke")
4462        .shape(EmitterShape::Disk { radius: 0.15 })
4463        .emission_rate(8.0)
4464        .max_particles(80)
4465        .lifetime(3.0, 5.0)
4466        .speed(0.3, 0.7)
4467        .size(0.5, 1.5)
4468        .blend(ParticleBlendMode::Alpha)
4469        .with_noise(0.5, 0.3)
4470        .build();
4471
4472    let embers = EmitterBuilder::new("Embers")
4473        .shape(EmitterShape::Cone { radius: 0.2, angle_deg: 20.0, length: 1.0 })
4474        .emission_rate(5.0)
4475        .max_particles(50)
4476        .lifetime(2.0, 5.0)
4477        .speed(0.5, 2.0)
4478        .size(0.02, 0.06)
4479        .blend(ParticleBlendMode::Additive)
4480        .with_noise(0.8, 0.6)
4481        .with_gravity(0.1)
4482        .build();
4483
4484    ParticleSystemBuilder::new("Campfire")
4485        .with_emitter(fire)
4486        .with_emitter(smoke)
4487        .with_emitter(embers)
4488        .build()
4489}
4490
4491pub fn build_waterfall() -> ParticleSystem {
4492    let water = EmitterBuilder::new("Water")
4493        .shape(EmitterShape::Line {
4494            start: Vec3::new(-1.0, 5.0, 0.0),
4495            end: Vec3::new(1.0, 5.0, 0.0)
4496        })
4497        .emission_rate(200.0)
4498        .max_particles(1000)
4499        .lifetime(1.5, 2.5)
4500        .speed(3.0, 5.0)
4501        .size(0.05, 0.15)
4502        .blend(ParticleBlendMode::Alpha)
4503        .with_gravity(1.5)
4504        .with_collision()
4505        .build();
4506
4507    let mist = EmitterBuilder::new("Mist")
4508        .shape(EmitterShape::Disk { radius: 2.0 })
4509        .emission_rate(20.0)
4510        .max_particles(100)
4511        .lifetime(3.0, 5.0)
4512        .speed(0.1, 0.3)
4513        .size(0.5, 2.0)
4514        .blend(ParticleBlendMode::Alpha)
4515        .with_noise(0.3, 0.2)
4516        .build();
4517
4518    ParticleSystemBuilder::new("Waterfall")
4519        .with_emitter(water)
4520        .with_emitter(mist)
4521        .with_force_field(make_wind_field(Vec3::new(0.2, 0.0, 0.0), 0.5))
4522        .build()
4523}
4524
4525pub fn build_rocket_exhaust() -> ParticleSystem {
4526    let exhaust = EmitterBuilder::new("Exhaust")
4527        .shape(EmitterShape::Disk { radius: 0.4 })
4528        .emission_rate(120.0)
4529        .max_particles(600)
4530        .lifetime(0.3, 0.8)
4531        .speed(8.0, 15.0)
4532        .size(0.1, 0.4)
4533        .blend(ParticleBlendMode::Additive)
4534        .with_gravity(0.0)
4535        .with_noise(2.0, 0.3)
4536        .color_gradient(fire_gradient())
4537        .build();
4538
4539    let smoke = EmitterBuilder::new("Exhaust Smoke")
4540        .shape(EmitterShape::Disk { radius: 0.3 })
4541        .emission_rate(30.0)
4542        .max_particles(200)
4543        .lifetime(1.0, 3.0)
4544        .speed(2.0, 5.0)
4545        .size(0.3, 1.2)
4546        .blend(ParticleBlendMode::Alpha)
4547        .with_noise(0.5, 0.5)
4548        .build();
4549
4550    ParticleSystemBuilder::new("Rocket Exhaust")
4551        .with_emitter(exhaust)
4552        .with_emitter(smoke)
4553        .build()
4554}
4555
4556// ============================================================
4557// EMITTER SHAPE HELPERS
4558// ============================================================
4559
4560impl EmitterShape {
4561    pub fn is_volumetric(&self) -> bool {
4562        match self {
4563            EmitterShape::Sphere { emit_from_shell: false, .. } => true,
4564            EmitterShape::Hemisphere { emit_from_shell: false, .. } => true,
4565            EmitterShape::Box { emit_from_shell: false, .. } => true,
4566            EmitterShape::Disk { .. } => false,
4567            _ => false,
4568        }
4569    }
4570    pub fn approximate_volume(&self) -> f32 {
4571        match self {
4572            EmitterShape::Sphere { radius, .. } => (4.0 / 3.0) * std::f32::consts::PI * radius * radius * radius,
4573            EmitterShape::Box { half_extents, .. } => half_extents.x * half_extents.y * half_extents.z * 8.0,
4574            EmitterShape::Cone { radius, length, .. } => std::f32::consts::PI * radius * radius * length / 3.0,
4575            EmitterShape::Ring { radius, tube_radius } => 2.0 * std::f32::consts::PI * std::f32::consts::PI * radius * tube_radius * tube_radius,
4576            EmitterShape::Disk { radius } => std::f32::consts::PI * radius * radius * 0.01,
4577            _ => 1.0,
4578        }
4579    }
4580}
4581
4582// ============================================================
4583// INTEGRATION HELPERS
4584// ============================================================
4585
4586pub fn rk4_step(pos: Vec3, vel: Vec3, accel: Vec3, dt: f32) -> (Vec3, Vec3) {
4587    let k1v = accel;
4588    let k1p = vel;
4589    let k2v = accel;
4590    let k2p = vel + k1v * (dt * 0.5);
4591    let k3v = accel;
4592    let k3p = vel + k2v * (dt * 0.5);
4593    let k4v = accel;
4594    let k4p = vel + k3v * dt;
4595    let new_vel = vel + (k1v + k2v * 2.0 + k3v * 2.0 + k4v) * (dt / 6.0);
4596    let new_pos = pos + (k1p + k2p * 2.0 + k3p * 2.0 + k4p) * (dt / 6.0);
4597    (new_pos, new_vel)
4598}
4599
4600// ============================================================
4601// FINAL MODULE DECLARATIONS
4602// ============================================================
4603
4604pub mod curves {
4605    pub use super::{CurveWrapMode, CurveKey, FloatCurve};
4606    pub use super::{GradientKey, ColorGradient};
4607    pub use super::{velocity_ease_in_out, size_burst_curve, pulse_curve};
4608    pub use super::{curve_multiply, curve_add, curve_inverse, curve_normalize};
4609}
4610
4611pub mod forces {
4612    pub use super::{
4613        DirectionalForce, VortexForceField, TurbulenceForce, DragForce,
4614        GravityPointForce, WindForce, MagneticForce, ForceField, ForceFieldKindInner,
4615        ForceFieldShape, ForceFieldKind, GravityType,
4616    };
4617    pub use super::{make_wind_field, make_gravity_well, make_repulsor, make_drag_field, make_turbulence_field, make_magnetic_field};
4618}
4619
4620pub mod noise {
4621    pub use super::{perlin3, fbm_noise, curl_noise, value_noise_1d, value_noise_2d, domain_warp};
4622}
4623
4624pub mod presets {
4625    pub use super::{
4626        EffectPreset,
4627        preset_fire, preset_smoke, preset_explosion, preset_rain, preset_sparks,
4628        preset_magic_trail, preset_snow, preset_dust, preset_bubbles, preset_electricity,
4629        preset_leaves, preset_blood_splatter, preset_vortex_portal, preset_healing_aura, preset_fireflies,
4630        build_campfire, build_waterfall, build_rocket_exhaust,
4631    };
4632}
4633
4634pub mod tests {
4635    pub use super::{
4636        test_curve_linear, test_curve_constant, test_gradient_lerp, test_spatial_hash,
4637        test_verlet, test_lorentz, test_perlin, test_rng_range, test_color_picker_hex,
4638        test_hsv_roundtrip, test_undo_redo, test_lod_system, test_emitter_shape_sphere,
4639        test_preset_creates_emitters, test_particle_age, test_force_field_magnetic_lorentz,
4640        test_vortex_force, test_texture_atlas, run_all_tests,
4641    };
4642}
4643
4644// ============================================================
4645// SPLINE PATH SYSTEM
4646// ============================================================
4647
4648#[derive(Clone, Debug)]
4649pub struct SplinePoint {
4650    pub position: Vec3,
4651    pub tangent_in: Vec3,
4652    pub tangent_out: Vec3,
4653    pub roll: f32,
4654    pub scale: f32,
4655}
4656
4657impl SplinePoint {
4658    pub fn new(pos: Vec3) -> Self {
4659        Self { position: pos, tangent_in: Vec3::ZERO, tangent_out: Vec3::ZERO, roll: 0.0, scale: 1.0 }
4660    }
4661    pub fn with_tangents(pos: Vec3, tin: Vec3, tout: Vec3) -> Self {
4662        Self { position: pos, tangent_in: tin, tangent_out: tout, roll: 0.0, scale: 1.0 }
4663    }
4664}
4665
4666#[derive(Clone, Debug)]
4667pub struct SplinePath {
4668    pub points: Vec<SplinePoint>,
4669    pub closed: bool,
4670    pub total_length: f32,
4671    pub segment_lengths: Vec<f32>,
4672}
4673
4674impl SplinePath {
4675    pub fn new() -> Self {
4676        Self { points: Vec::new(), closed: false, total_length: 0.0, segment_lengths: Vec::new() }
4677    }
4678    pub fn add_point(&mut self, p: SplinePoint) {
4679        self.points.push(p);
4680        self.recalculate_lengths();
4681    }
4682    pub fn recalculate_lengths(&mut self) {
4683        self.segment_lengths.clear();
4684        self.total_length = 0.0;
4685        let n = if self.closed { self.points.len() } else { self.points.len().saturating_sub(1) };
4686        for i in 0..n {
4687            let j = (i + 1) % self.points.len();
4688            let len = self.approximate_segment_length(i, j);
4689            self.segment_lengths.push(len);
4690            self.total_length += len;
4691        }
4692    }
4693    fn approximate_segment_length(&self, i: usize, j: usize) -> f32 {
4694        let steps = 16u32;
4695        let mut len = 0.0_f32;
4696        let mut prev = self.evaluate_at_segment(i, j, 0.0);
4697        for s in 1..=steps {
4698            let t = s as f32 / steps as f32;
4699            let curr = self.evaluate_at_segment(i, j, t);
4700            len += (curr - prev).length();
4701            prev = curr;
4702        }
4703        len
4704    }
4705    pub fn evaluate_at_segment(&self, i: usize, j: usize, t: f32) -> Vec3 {
4706        let p0 = self.points[i].position;
4707        let p1 = self.points[j].position;
4708        let m0 = self.points[i].tangent_out;
4709        let m1 = self.points[j].tangent_in;
4710        let t2 = t * t; let t3 = t2 * t;
4711        let h00 = 2.0*t3 - 3.0*t2 + 1.0;
4712        let h10 = t3 - 2.0*t2 + t;
4713        let h01 = -2.0*t3 + 3.0*t2;
4714        let h11 = t3 - t2;
4715        h00*p0 + h10*m0 + h01*p1 + h11*m1
4716    }
4717    pub fn evaluate_at_distance(&self, distance: f32) -> (Vec3, Vec3) {
4718        if self.points.is_empty() { return (Vec3::ZERO, Vec3::Z); }
4719        if self.points.len() == 1 { return (self.points[0].position, Vec3::Z); }
4720        let dist = distance.rem_euclid(self.total_length.max(0.001));
4721        let mut accum = 0.0_f32;
4722        for (seg_idx, &seg_len) in self.segment_lengths.iter().enumerate() {
4723            if accum + seg_len >= dist || seg_idx == self.segment_lengths.len() - 1 {
4724                let local_t = if seg_len > 0.0 { (dist - accum) / seg_len } else { 0.0 };
4725                let j = (seg_idx + 1) % self.points.len();
4726                let pos = self.evaluate_at_segment(seg_idx, j, local_t.clamp(0.0, 1.0));
4727                let tangent_pos = self.evaluate_at_segment(seg_idx, j, (local_t + 0.01).clamp(0.0, 1.0));
4728                let tangent = (tangent_pos - pos).normalize_or_zero();
4729                return (pos, tangent);
4730            }
4731            accum += seg_len;
4732        }
4733        (self.points.last().unwrap().position, Vec3::Z)
4734    }
4735    pub fn set_auto_tangents(&mut self) {
4736        let n = self.points.len();
4737        if n < 2 { return; }
4738        for i in 0..n {
4739            let prev = if i == 0 {
4740                if self.closed { n - 1 } else { 0 }
4741            } else { i - 1 };
4742            let next = if i == n - 1 {
4743                if self.closed { 0 } else { n - 1 }
4744            } else { i + 1 };
4745            let dir = (self.points[next].position - self.points[prev].position) * 0.5;
4746            self.points[i].tangent_in = dir;
4747            self.points[i].tangent_out = dir;
4748        }
4749    }
4750}
4751
4752// ============================================================
4753// TRAIL RENDERER STATE
4754// ============================================================
4755
4756#[derive(Clone, Debug)]
4757pub struct TrailPoint {
4758    pub position: Vec3,
4759    pub time: f32,
4760    pub width: f32,
4761    pub color: Vec4,
4762}
4763
4764#[derive(Clone, Debug)]
4765pub struct TrailRenderer {
4766    pub points: VecDeque<TrailPoint>,
4767    pub max_points: usize,
4768    pub lifetime: f32,
4769    pub width_curve: FloatCurve,
4770    pub color_gradient: ColorGradient,
4771    pub min_vertex_distance: f32,
4772    pub enabled: bool,
4773}
4774
4775impl TrailRenderer {
4776    pub fn new(max_points: usize, lifetime: f32) -> Self {
4777        Self {
4778            points: VecDeque::new(),
4779            max_points,
4780            lifetime,
4781            width_curve: FloatCurve::linear(0.1, 0.0),
4782            color_gradient: alpha_fade_gradient(),
4783            min_vertex_distance: 0.05,
4784            enabled: true,
4785        }
4786    }
4787    pub fn emit(&mut self, pos: Vec3, time: f32) {
4788        if let Some(last) = self.points.back() {
4789            if (pos - last.position).length() < self.min_vertex_distance { return; }
4790        }
4791        if self.points.len() >= self.max_points { self.points.pop_front(); }
4792        self.points.push_back(TrailPoint { position: pos, time, width: 0.1, color: Vec4::ONE });
4793    }
4794    pub fn update(&mut self, current_time: f32) {
4795        self.points.retain(|p| current_time - p.time < self.lifetime);
4796        let n = self.points.len();
4797        for (i, p) in self.points.iter_mut().enumerate() {
4798            let t = i as f32 / n.max(1) as f32;
4799            p.width = self.width_curve.evaluate(t);
4800            p.color = self.color_gradient.evaluate(t);
4801        }
4802    }
4803    pub fn vertex_count(&self) -> usize { self.points.len() * 2 }
4804}
4805
4806// ============================================================
4807// RIBBON RENDERER STATE
4808// ============================================================
4809
4810#[derive(Clone, Debug)]
4811pub struct RibbonVertex {
4812    pub position: Vec3,
4813    pub uv: Vec2,
4814    pub color: Vec4,
4815    pub normal: Vec3,
4816}
4817
4818#[derive(Clone, Debug)]
4819pub struct RibbonRenderer {
4820    pub vertices: Vec<RibbonVertex>,
4821    pub indices: Vec<u32>,
4822    pub width: f32,
4823    pub segments: u32,
4824    pub uv_tiling: f32,
4825    pub face_camera: bool,
4826}
4827
4828impl RibbonRenderer {
4829    pub fn new(width: f32, segments: u32) -> Self {
4830        Self {
4831            vertices: Vec::new(),
4832            indices: Vec::new(),
4833            width,
4834            segments,
4835            uv_tiling: 1.0,
4836            face_camera: true,
4837        }
4838    }
4839    pub fn build_from_path(&mut self, path: &[Vec3], camera_pos: Vec3, color: Vec4) {
4840        self.vertices.clear();
4841        self.indices.clear();
4842        if path.len() < 2 { return; }
4843        for (i, &pos) in path.iter().enumerate() {
4844            let t = i as f32 / (path.len() - 1) as f32;
4845            let forward = if i + 1 < path.len() {
4846                (path[i + 1] - pos).normalize_or_zero()
4847            } else {
4848                (pos - path[i - 1]).normalize_or_zero()
4849            };
4850            let to_cam = (camera_pos - pos).normalize_or_zero();
4851            let right = forward.cross(to_cam).normalize_or_zero();
4852            let half_w = self.width * 0.5;
4853            let uv_x = t * self.uv_tiling;
4854            self.vertices.push(RibbonVertex { position: pos - right * half_w, uv: Vec2::new(uv_x, 0.0), color, normal: to_cam });
4855            self.vertices.push(RibbonVertex { position: pos + right * half_w, uv: Vec2::new(uv_x, 1.0), color, normal: to_cam });
4856            if i > 0 {
4857                let base = ((i - 1) * 2) as u32;
4858                self.indices.extend_from_slice(&[base, base+1, base+2, base+1, base+3, base+2]);
4859            }
4860        }
4861    }
4862}
4863
4864// ============================================================
4865// PARTICLE COLLISION RESPONSE EXTENDED
4866// ============================================================
4867
4868#[derive(Clone, Debug)]
4869pub struct CollisionResult {
4870    pub collided: bool,
4871    pub position: Vec3,
4872    pub normal: Vec3,
4873    pub penetration: f32,
4874}
4875
4876pub fn sphere_plane_collision(center: Vec3, radius: f32, plane_normal: Vec3, plane_dist: f32) -> Option<CollisionResult> {
4877    let d = plane_normal.dot(center) - plane_dist;
4878    if d < radius {
4879        Some(CollisionResult {
4880            collided: true,
4881            position: center - plane_normal * (d - radius),
4882            normal: plane_normal,
4883            penetration: radius - d,
4884        })
4885    } else {
4886        None
4887    }
4888}
4889
4890pub fn sphere_sphere_collision(c0: Vec3, r0: f32, c1: Vec3, r1: f32) -> Option<CollisionResult> {
4891    let diff = c0 - c1;
4892    let dist = diff.length();
4893    let min_dist = r0 + r1;
4894    if dist < min_dist && dist > 0.0001 {
4895        let normal = diff / dist;
4896        Some(CollisionResult {
4897            collided: true,
4898            position: c1 + normal * r1,
4899            normal,
4900            penetration: min_dist - dist,
4901        })
4902    } else {
4903        None
4904    }
4905}
4906
4907// ============================================================
4908// PARTICLE POOL
4909// ============================================================
4910
4911#[derive(Clone, Debug)]
4912pub struct ParticlePool {
4913    pub free_indices: Vec<usize>,
4914    pub particles: Vec<Particle>,
4915    pub capacity: usize,
4916}
4917
4918impl ParticlePool {
4919    pub fn new(capacity: usize) -> Self {
4920        let dummy = Particle::new(Vec3::ZERO, Vec3::ZERO, 1.0, Vec4::ONE, 0.1);
4921        let mut pool = Self {
4922            free_indices: (0..capacity).collect(),
4923            particles: vec![dummy; capacity],
4924            capacity,
4925        };
4926        for p in &mut pool.particles { p.alive = false; }
4927        pool
4928    }
4929    pub fn allocate(&mut self) -> Option<usize> {
4930        self.free_indices.pop()
4931    }
4932    pub fn free(&mut self, idx: usize) {
4933        self.particles[idx].alive = false;
4934        self.free_indices.push(idx);
4935    }
4936    pub fn alive_count(&self) -> usize {
4937        self.capacity - self.free_indices.len()
4938    }
4939    pub fn is_full(&self) -> bool { self.free_indices.is_empty() }
4940}
4941
4942// ============================================================
4943// ANIMATION CLIP FOR EMITTER PROPERTIES
4944// ============================================================
4945
4946#[derive(Clone, Debug)]
4947pub struct EmitterAnimTrack {
4948    pub property: EmitterAnimProperty,
4949    pub curve: FloatCurve,
4950}
4951
4952#[derive(Clone, Copy, Debug, PartialEq)]
4953pub enum EmitterAnimProperty {
4954    EmissionRate,
4955    SizeMultiplier,
4956    SpeedMultiplier,
4957    OpacityMultiplier,
4958    GravityMultiplier,
4959    NoiseAmplitude,
4960}
4961
4962impl EmitterAnimProperty {
4963    pub fn name(&self) -> &'static str {
4964        match self {
4965            EmitterAnimProperty::EmissionRate => "Emission Rate",
4966            EmitterAnimProperty::SizeMultiplier => "Size Multiplier",
4967            EmitterAnimProperty::SpeedMultiplier => "Speed Multiplier",
4968            EmitterAnimProperty::OpacityMultiplier => "Opacity Multiplier",
4969            EmitterAnimProperty::GravityMultiplier => "Gravity Multiplier",
4970            EmitterAnimProperty::NoiseAmplitude => "Noise Amplitude",
4971        }
4972    }
4973}
4974
4975#[derive(Clone, Debug)]
4976pub struct EmitterAnimClip {
4977    pub name: String,
4978    pub duration: f32,
4979    pub looping: bool,
4980    pub tracks: Vec<EmitterAnimTrack>,
4981    pub time: f32,
4982    pub playing: bool,
4983}
4984
4985impl EmitterAnimClip {
4986    pub fn new(name: &str, duration: f32) -> Self {
4987        Self { name: name.to_string(), duration, looping: true, tracks: Vec::new(), time: 0.0, playing: false }
4988    }
4989    pub fn add_track(&mut self, property: EmitterAnimProperty, curve: FloatCurve) {
4990        self.tracks.push(EmitterAnimTrack { property, curve });
4991    }
4992    pub fn evaluate(&self, property: EmitterAnimProperty) -> Option<f32> {
4993        self.tracks.iter().find(|t| t.property == property).map(|t| t.curve.evaluate(self.time / self.duration.max(0.001)))
4994    }
4995    pub fn update(&mut self, dt: f32) {
4996        if !self.playing { return; }
4997        self.time += dt;
4998        if self.time >= self.duration {
4999            if self.looping { self.time -= self.duration; } else { self.time = self.duration; self.playing = false; }
5000        }
5001    }
5002    pub fn apply_to_emitter(&self, e: &mut ParticleEmitter) {
5003        if let Some(v) = self.evaluate(EmitterAnimProperty::EmissionRate) { e.emission_rate = v; }
5004        if let Some(v) = self.evaluate(EmitterAnimProperty::GravityMultiplier) { e.gravity_module.gravity_multiplier = v; }
5005        if let Some(v) = self.evaluate(EmitterAnimProperty::NoiseAmplitude) { e.noise_module.amplitude = v; }
5006    }
5007}
5008
5009// ============================================================
5010// EXTENDED EDITOR HISTORY MANAGEMENT
5011// ============================================================
5012
5013#[derive(Clone, Debug)]
5014pub struct HistoryBranch {
5015    pub id: u64,
5016    pub parent_id: Option<u64>,
5017    pub label: String,
5018    pub actions: Vec<ParticleEditorAction>,
5019}
5020
5021#[derive(Clone, Debug)]
5022pub struct HistoryTree {
5023    pub branches: Vec<HistoryBranch>,
5024    pub current_branch: u64,
5025    pub next_branch_id: u64,
5026}
5027
5028impl HistoryTree {
5029    pub fn new() -> Self {
5030        Self {
5031            branches: vec![HistoryBranch { id: 1, parent_id: None, label: "Main".to_string(), actions: Vec::new() }],
5032            current_branch: 1,
5033            next_branch_id: 2,
5034        }
5035    }
5036    pub fn current(&self) -> Option<&HistoryBranch> {
5037        self.branches.iter().find(|b| b.id == self.current_branch)
5038    }
5039    pub fn branch_count(&self) -> usize { self.branches.len() }
5040}
5041
5042// ============================================================
5043// PARTICLE SYSTEM LOD MANAGER
5044// ============================================================
5045
5046#[derive(Clone, Debug)]
5047pub struct LodManager {
5048    pub lod_systems: HashMap<u64, LodSystem>,
5049    pub camera_position: Vec3,
5050    pub global_scale: f32,
5051}
5052
5053impl LodManager {
5054    pub fn new() -> Self {
5055        Self { lod_systems: HashMap::new(), camera_position: Vec3::ZERO, global_scale: 1.0 }
5056    }
5057    pub fn register(&mut self, system_id: u64, lod: LodSystem) {
5058        self.lod_systems.insert(system_id, lod);
5059    }
5060    pub fn get_emission_scale(&self, system_id: u64, system_pos: Vec3) -> f32 {
5061        let dist = (system_pos - self.camera_position).length() * self.global_scale;
5062        self.lod_systems.get(&system_id).map(|l| l.get_emission_scale(dist)).unwrap_or(1.0)
5063    }
5064    pub fn set_camera(&mut self, pos: Vec3) { self.camera_position = pos; }
5065}
5066
5067// ============================================================
5068// DEBUG VISUALIZER DATA
5069// ============================================================
5070
5071#[derive(Clone, Debug)]
5072pub struct DebugLine {
5073    pub start: Vec3,
5074    pub end: Vec3,
5075    pub color: Vec4,
5076    pub duration: f32,
5077    pub elapsed: f32,
5078}
5079
5080#[derive(Clone, Debug)]
5081pub struct DebugSphere {
5082    pub center: Vec3,
5083    pub radius: f32,
5084    pub color: Vec4,
5085    pub duration: f32,
5086    pub elapsed: f32,
5087    pub wire: bool,
5088}
5089
5090#[derive(Clone, Debug)]
5091pub struct DebugVisualizer {
5092    pub lines: Vec<DebugLine>,
5093    pub spheres: Vec<DebugSphere>,
5094    pub enabled: bool,
5095}
5096
5097impl DebugVisualizer {
5098    pub fn new() -> Self { Self { lines: Vec::new(), spheres: Vec::new(), enabled: true } }
5099    pub fn draw_line(&mut self, start: Vec3, end: Vec3, color: Vec4, duration: f32) {
5100        self.lines.push(DebugLine { start, end, color, duration, elapsed: 0.0 });
5101    }
5102    pub fn draw_sphere(&mut self, center: Vec3, radius: f32, color: Vec4, wire: bool, duration: f32) {
5103        self.spheres.push(DebugSphere { center, radius, color, duration, elapsed: 0.0, wire });
5104    }
5105    pub fn draw_box_wire(&mut self, center: Vec3, half: Vec3, color: Vec4, duration: f32) {
5106        let corners = [
5107            center + Vec3::new( half.x,  half.y,  half.z),
5108            center + Vec3::new(-half.x,  half.y,  half.z),
5109            center + Vec3::new(-half.x, -half.y,  half.z),
5110            center + Vec3::new( half.x, -half.y,  half.z),
5111            center + Vec3::new( half.x,  half.y, -half.z),
5112            center + Vec3::new(-half.x,  half.y, -half.z),
5113            center + Vec3::new(-half.x, -half.y, -half.z),
5114            center + Vec3::new( half.x, -half.y, -half.z),
5115        ];
5116        let edges = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)];
5117        for (a, b) in edges { self.draw_line(corners[a], corners[b], color, duration); }
5118    }
5119    pub fn update(&mut self, dt: f32) {
5120        for l in &mut self.lines { l.elapsed += dt; }
5121        for s in &mut self.spheres { s.elapsed += dt; }
5122        self.lines.retain(|l| l.elapsed < l.duration || l.duration < 0.0);
5123        self.spheres.retain(|s| s.elapsed < s.duration || s.duration < 0.0);
5124    }
5125    pub fn visualize_emitter_shape(&mut self, emitter: &ParticleEmitter) {
5126        let color = Vec4::new(0.0, 1.0, 0.0, 0.5);
5127        match &emitter.shape {
5128            EmitterShape::Sphere { radius, .. } => {
5129                self.draw_sphere(emitter.position, *radius, color, true, -1.0);
5130            }
5131            EmitterShape::Box { half_extents, .. } => {
5132                self.draw_box_wire(emitter.position, *half_extents, color, -1.0);
5133            }
5134            EmitterShape::Point => {
5135                self.draw_sphere(emitter.position, 0.1, color, true, -1.0);
5136            }
5137            _ => {}
5138        }
5139    }
5140    pub fn visualize_force_field(&mut self, ff: &ForceField) {
5141        let color = Vec4::new(0.0, 0.5, 1.0, 0.5);
5142        match &ff.shape {
5143            ForceFieldShape::Sphere { radius } => {
5144                self.draw_sphere(ff.center, *radius, color, true, -1.0);
5145            }
5146            ForceFieldShape::Box { half_extents } => {
5147                self.draw_box_wire(ff.center, *half_extents, color, -1.0);
5148            }
5149            _ => {}
5150        }
5151    }
5152}
5153
5154// ============================================================
5155// ASSET LIBRARY
5156// ============================================================
5157
5158#[derive(Clone, Debug)]
5159pub struct AssetLibrary {
5160    pub assets: HashMap<u64, ParticleEffectAsset>,
5161    pub next_id: u64,
5162    pub tags: BTreeMap<String, Vec<u64>>,
5163}
5164
5165impl AssetLibrary {
5166    pub fn new() -> Self {
5167        Self { assets: HashMap::new(), next_id: 1, tags: BTreeMap::new() }
5168    }
5169    pub fn add(&mut self, mut asset: ParticleEffectAsset) -> u64 {
5170        let id = self.next_id;
5171        self.next_id += 1;
5172        asset.id = id;
5173        for tag in &asset.tags {
5174            self.tags.entry(tag.clone()).or_default().push(id);
5175        }
5176        self.assets.insert(id, asset);
5177        id
5178    }
5179    pub fn get(&self, id: u64) -> Option<&ParticleEffectAsset> { self.assets.get(&id) }
5180    pub fn get_mut(&mut self, id: u64) -> Option<&mut ParticleEffectAsset> { self.assets.get_mut(&id) }
5181    pub fn remove(&mut self, id: u64) -> Option<ParticleEffectAsset> {
5182        if let Some(asset) = self.assets.remove(&id) {
5183            for tag in &asset.tags {
5184                if let Some(ids) = self.tags.get_mut(tag) {
5185                    ids.retain(|&i| i != id);
5186                }
5187            }
5188            Some(asset)
5189        } else {
5190            None
5191        }
5192    }
5193    pub fn search_by_tag(&self, tag: &str) -> Vec<u64> {
5194        self.tags.get(tag).cloned().unwrap_or_default()
5195    }
5196    pub fn search_by_name(&self, query: &str) -> Vec<u64> {
5197        let q = query.to_lowercase();
5198        self.assets.iter()
5199            .filter(|(_, a)| a.name.to_lowercase().contains(&q))
5200            .map(|(&id, _)| id)
5201            .collect()
5202    }
5203    pub fn count(&self) -> usize { self.assets.len() }
5204}
5205
5206// ============================================================
5207// PARTICLE SYSTEM SEQUENCER
5208// ============================================================
5209
5210#[derive(Clone, Debug)]
5211pub struct SequencerEvent {
5212    pub time: f32,
5213    pub system_id: u64,
5214    pub action: SequencerAction,
5215}
5216
5217#[derive(Clone, Debug)]
5218pub enum SequencerAction {
5219    Play,
5220    Stop,
5221    Pause,
5222    SetEmissionRate(u64, f32),
5223    Burst(u64, u32),
5224    EnableEmitter(u64, bool),
5225    SetTimeScale(f32),
5226}
5227
5228#[derive(Clone, Debug)]
5229pub struct ParticleSequencer {
5230    pub events: Vec<SequencerEvent>,
5231    pub time: f32,
5232    pub duration: f32,
5233    pub looping: bool,
5234    pub playing: bool,
5235    pub next_event_idx: usize,
5236}
5237
5238impl ParticleSequencer {
5239    pub fn new(duration: f32) -> Self {
5240        Self { events: Vec::new(), time: 0.0, duration, looping: false, playing: false, next_event_idx: 0 }
5241    }
5242    pub fn add_event(&mut self, time: f32, system_id: u64, action: SequencerAction) {
5243        self.events.push(SequencerEvent { time, system_id, action });
5244        self.events.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
5245    }
5246    pub fn play(&mut self) { self.playing = true; }
5247    pub fn stop(&mut self) { self.playing = false; self.time = 0.0; self.next_event_idx = 0; }
5248    pub fn update(&mut self, dt: f32) -> Vec<&SequencerEvent> {
5249        if !self.playing { return Vec::new(); }
5250        self.time += dt;
5251        let mut triggered = Vec::new();
5252        while self.next_event_idx < self.events.len() && self.events[self.next_event_idx].time <= self.time {
5253            triggered.push(&self.events[self.next_event_idx]);
5254            self.next_event_idx += 1;
5255        }
5256        if self.time >= self.duration {
5257            if self.looping {
5258                self.time -= self.duration;
5259                self.next_event_idx = 0;
5260            } else {
5261                self.playing = false;
5262            }
5263        }
5264        triggered
5265    }
5266}
5267
5268// ============================================================
5269// PARTICLE SYSTEM TEMPLATES
5270// ============================================================
5271
5272#[derive(Clone, Debug)]
5273pub struct ParticleTemplate {
5274    pub name: String,
5275    pub category: String,
5276    pub description: String,
5277    pub thumbnail_id: Option<u64>,
5278    pub tags: Vec<String>,
5279    pub factory: TemplateFactory,
5280}
5281
5282#[derive(Clone, Debug)]
5283pub enum TemplateFactory {
5284    Preset(EffectPreset),
5285    Custom(String),
5286    Procedural { emitter_count: u32, max_particles_each: u32 },
5287}
5288
5289impl ParticleTemplate {
5290    pub fn from_preset(preset: EffectPreset) -> Self {
5291        Self {
5292            name: preset.name().to_string(),
5293            category: "Built-in".to_string(),
5294            description: format!("Built-in {} particle effect", preset.name()),
5295            thumbnail_id: None,
5296            tags: vec![preset.name().to_lowercase()],
5297            factory: TemplateFactory::Preset(preset),
5298        }
5299    }
5300    pub fn all_builtin() -> Vec<ParticleTemplate> {
5301        EffectPreset::all().iter().map(|&p| Self::from_preset(p)).collect()
5302    }
5303}
5304
5305// ============================================================
5306// PARTICLE EFFECT INSTANCE MANAGER
5307// ============================================================
5308
5309#[derive(Clone, Debug)]
5310pub struct EffectInstance {
5311    pub id: u64,
5312    pub system: ParticleSystem,
5313    pub world_position: Vec3,
5314    pub world_rotation: Quat,
5315    pub world_scale: f32,
5316    pub alive: bool,
5317    pub auto_destroy: bool,
5318    pub created_at: f32,
5319}
5320
5321impl EffectInstance {
5322    pub fn new(id: u64, system: ParticleSystem, pos: Vec3) -> Self {
5323        Self {
5324            id,
5325            system,
5326            world_position: pos,
5327            world_rotation: Quat::IDENTITY,
5328            world_scale: 1.0,
5329            alive: true,
5330            auto_destroy: true,
5331            created_at: 0.0,
5332        }
5333    }
5334    pub fn is_finished(&self) -> bool {
5335        if !self.auto_destroy { return false; }
5336        self.system.total_alive() == 0 && self.system.emitters.iter().all(|e| !e.is_playing)
5337    }
5338}
5339
5340#[derive(Clone, Debug)]
5341pub struct EffectInstanceManager {
5342    pub instances: Vec<EffectInstance>,
5343    pub next_id: u64,
5344    pub max_instances: usize,
5345}
5346
5347impl EffectInstanceManager {
5348    pub fn new(max: usize) -> Self {
5349        Self { instances: Vec::new(), next_id: 1, max_instances: max }
5350    }
5351    pub fn spawn(&mut self, system: ParticleSystem, pos: Vec3) -> Option<u64> {
5352        if self.instances.len() >= self.max_instances { return None; }
5353        let id = self.next_id;
5354        self.next_id += 1;
5355        let mut inst = EffectInstance::new(id, system, pos);
5356        inst.system.play_all();
5357        self.instances.push(inst);
5358        Some(id)
5359    }
5360    pub fn update(&mut self, dt: f32) {
5361        for inst in &mut self.instances {
5362            inst.system.world_position = inst.world_position;
5363            inst.system.update(dt);
5364        }
5365        self.instances.retain(|i| i.alive && !i.is_finished());
5366    }
5367    pub fn destroy(&mut self, id: u64) {
5368        if let Some(inst) = self.instances.iter_mut().find(|i| i.id == id) {
5369            inst.alive = false;
5370        }
5371    }
5372    pub fn count(&self) -> usize { self.instances.len() }
5373    pub fn total_particles(&self) -> u32 {
5374        self.instances.iter().map(|i| i.system.total_alive()).sum()
5375    }
5376}
5377
5378// ============================================================
5379// EXTRA NOISE HELPERS
5380// ============================================================
5381
5382pub fn simplex_noise_3d_approx(p: Vec3) -> f32 {
5383    // Approximate simplex using skewed Perlin lattice
5384    let s = (p.x + p.y + p.z) * (1.0 / 3.0);
5385    let skewed = p + Vec3::splat(s);
5386    perlin3(skewed) * 2.0 - 1.0
5387}
5388
5389pub fn worley_noise_3d(p: Vec3, cell_count: f32) -> f32 {
5390    let scaled = p * cell_count;
5391    let base = Vec3::new(scaled.x.floor(), scaled.y.floor(), scaled.z.floor());
5392    let mut min_dist = f32::INFINITY;
5393    for dx in -1i32..=1 {
5394        for dy in -1i32..=1 {
5395            for dz in -1i32..=1 {
5396                let cell = base + Vec3::new(dx as f32, dy as f32, dz as f32);
5397                let hx = (PERM[((cell.x as i32 + cell.y as i32 * 31 + cell.z as i32 * 97) & 511) as usize] as f32) / 255.0;
5398                let hy = (PERM[((cell.x as i32 * 7 + cell.y as i32 + cell.z as i32 * 53) & 511) as usize] as f32) / 255.0;
5399                let hz = (PERM[((cell.x as i32 * 13 + cell.y as i32 * 43 + cell.z as i32) & 511) as usize] as f32) / 255.0;
5400                let candidate = cell + Vec3::new(hx, hy, hz);
5401                let d = (scaled - candidate).length();
5402                if d < min_dist { min_dist = d; }
5403            }
5404        }
5405    }
5406    min_dist.clamp(0.0, 1.0)
5407}
5408
5409pub fn ridge_noise(p: Vec3, octaves: u32, persistence: f32, lacunarity: f32) -> f32 {
5410    let raw = fbm_noise(p, octaves, persistence, lacunarity);
5411    1.0 - (raw * 2.0 - 1.0).abs()
5412}
5413
5414// ============================================================
5415// PARTICLE SYSTEM CONFIG FILE HELPERS
5416// ============================================================
5417
5418#[derive(Clone, Debug)]
5419pub struct ParticleConfig {
5420    pub global_gravity: Vec3,
5421    pub global_time_scale: f32,
5422    pub max_total_particles: u32,
5423    pub lod_bias: f32,
5424    pub enable_collision: bool,
5425    pub enable_noise: bool,
5426    pub thread_count: u32,
5427    pub use_gpu_simulation: bool,
5428}
5429
5430impl Default for ParticleConfig {
5431    fn default() -> Self {
5432        Self {
5433            global_gravity: Vec3::new(0.0, -9.81, 0.0),
5434            global_time_scale: 1.0,
5435            max_total_particles: 100000,
5436            lod_bias: 1.0,
5437            enable_collision: true,
5438            enable_noise: true,
5439            thread_count: 4,
5440            use_gpu_simulation: false,
5441        }
5442    }
5443}
5444
5445impl ParticleConfig {
5446    pub fn low_quality() -> Self {
5447        Self { max_total_particles: 10000, enable_noise: false, lod_bias: 2.0, ..Default::default() }
5448    }
5449    pub fn high_quality() -> Self {
5450        Self { max_total_particles: 500000, use_gpu_simulation: true, lod_bias: 0.5, ..Default::default() }
5451    }
5452}
5453
5454// ── Particle Attractor & Force Field System ───────────────────────────────────
5455
5456#[derive(Clone, Debug)]
5457pub struct ParticleAttractor {
5458    pub id: u32,
5459    pub position: Vec3,
5460    pub strength: f32,
5461    pub radius: f32,
5462    pub attractor_type: AttractorType,
5463    pub falloff: f32,
5464    pub enabled: bool,
5465}
5466
5467#[derive(Clone, Debug, PartialEq)]
5468pub enum AttractorType {
5469    Attract,
5470    Repel,
5471    Vortex,
5472    Drag,
5473    Gravity,
5474    Wind,
5475}
5476
5477impl ParticleAttractor {
5478    pub fn new(id: u32, position: Vec3, strength: f32, radius: f32, attractor_type: AttractorType) -> Self {
5479        Self { id, position, strength, radius, attractor_type, falloff: 2.0, enabled: true }
5480    }
5481    pub fn force_at(&self, particle_pos: Vec3) -> Vec3 {
5482        let delta = self.position - particle_pos;
5483        let dist = delta.length();
5484        if dist > self.radius || dist < 1e-5 { return Vec3::ZERO; }
5485        let factor = (1.0 - dist / self.radius).powf(self.falloff);
5486        match self.attractor_type {
5487            AttractorType::Attract => delta.normalize() * self.strength * factor,
5488            AttractorType::Repel => -delta.normalize() * self.strength * factor,
5489            AttractorType::Drag => Vec3::ZERO,
5490            AttractorType::Vortex => {
5491                let perp = Vec3::new(-delta.z, 0.0, delta.x).normalize();
5492                perp * self.strength * factor
5493            }
5494            _ => delta.normalize() * self.strength * factor,
5495        }
5496    }
5497    pub fn in_range(&self, pos: Vec3) -> bool { (self.position - pos).length() <= self.radius }
5498}
5499
5500#[derive(Clone, Debug)]
5501pub struct ParticleForceField {
5502    pub id: u32,
5503    pub name: String,
5504    pub bounds: (Vec3, Vec3),
5505    pub force: Vec3,
5506    pub field_type: String,
5507    pub enabled: bool,
5508    pub turbulence: f32,
5509}
5510
5511impl ParticleForceField {
5512    pub fn new(id: u32, name: impl Into<String>, min: Vec3, max: Vec3, force: Vec3) -> Self {
5513        Self { id, name: name.into(), bounds: (min, max), force, field_type: "constant".into(), enabled: true, turbulence: 0.0 }
5514    }
5515    pub fn contains(&self, pos: Vec3) -> bool {
5516        pos.x >= self.bounds.0.x && pos.x <= self.bounds.1.x &&
5517        pos.y >= self.bounds.0.y && pos.y <= self.bounds.1.y &&
5518        pos.z >= self.bounds.0.z && pos.z <= self.bounds.1.z
5519    }
5520    pub fn apply(&self, pos: Vec3) -> Vec3 {
5521        if !self.enabled || !self.contains(pos) { Vec3::ZERO } else { self.force }
5522    }
5523}
5524
5525// ── Particle Trail System ─────────────────────────────────────────────────────
5526
5527#[derive(Clone, Debug)]
5528pub struct ParticleTrail {
5529    pub id: u32,
5530    pub parent_particle_id: u32,
5531    pub positions: VecDeque<Vec3>,
5532    pub max_length: usize,
5533    pub color_start: Vec4,
5534    pub color_end: Vec4,
5535    pub width_start: f32,
5536    pub width_end: f32,
5537    pub fade_time: f32,
5538    pub emit_rate: f32,
5539    pub enabled: bool,
5540}
5541
5542impl ParticleTrail {
5543    pub fn new(id: u32, parent_id: u32, max_length: usize) -> Self {
5544        Self { id, parent_particle_id: parent_id, positions: VecDeque::new(), max_length, color_start: Vec4::ONE, color_end: Vec4::new(1.0, 1.0, 1.0, 0.0), width_start: 0.1, width_end: 0.01, fade_time: 1.0, emit_rate: 30.0, enabled: true }
5545    }
5546    pub fn update(&mut self, new_pos: Vec3) {
5547        self.positions.push_back(new_pos);
5548        while self.positions.len() > self.max_length { self.positions.pop_front(); }
5549    }
5550    pub fn length(&self) -> usize { self.positions.len() }
5551    pub fn is_empty(&self) -> bool { self.positions.is_empty() }
5552    pub fn clear(&mut self) { self.positions.clear(); }
5553    pub fn last_pos(&self) -> Option<Vec3> { self.positions.back().copied() }
5554}
5555
5556// ── Particle Collision System ─────────────────────────────────────────────────
5557
5558#[derive(Clone, Debug)]
5559pub struct ParticleCollider {
5560    pub id: u32,
5561    pub collider_type: ColliderShape,
5562    pub position: Vec3,
5563    pub rotation: Quat,
5564    pub restitution: f32,
5565    pub friction: f32,
5566    pub kill_on_hit: bool,
5567    pub spawn_on_hit: Option<u32>,
5568}
5569
5570#[derive(Clone, Debug, PartialEq)]
5571pub enum ColliderShape {
5572    Sphere { radius: f32 },
5573    Box { half_extents: Vec3 },
5574    Plane { normal: Vec3, d: f32 },
5575    Cylinder { radius: f32, height: f32 },
5576}
5577
5578impl ParticleCollider {
5579    pub fn sphere(id: u32, pos: Vec3, radius: f32) -> Self {
5580        Self { id, collider_type: ColliderShape::Sphere { radius }, position: pos, rotation: Quat::IDENTITY, restitution: 0.5, friction: 0.3, kill_on_hit: false, spawn_on_hit: None }
5581    }
5582    pub fn plane(id: u32, normal: Vec3, d: f32) -> Self {
5583        Self { id, collider_type: ColliderShape::Plane { normal, d }, position: Vec3::ZERO, rotation: Quat::IDENTITY, restitution: 0.3, friction: 0.5, kill_on_hit: false, spawn_on_hit: None }
5584    }
5585    pub fn test_sphere(&self, center: Vec3, radius: f32) -> bool {
5586        match &self.collider_type {
5587            ColliderShape::Sphere { radius: r } => (center - self.position).length() < r + radius,
5588            ColliderShape::Plane { normal, d } => normal.dot(center) + d < radius,
5589            ColliderShape::Box { half_extents } => {
5590                let local = center - self.position;
5591                local.x.abs() < half_extents.x + radius && local.y.abs() < half_extents.y + radius && local.z.abs() < half_extents.z + radius
5592            }
5593            _ => false,
5594        }
5595    }
5596}
5597
5598// ── Particle LOD System ───────────────────────────────────────────────────────
5599
5600#[derive(Clone, Debug)]
5601pub struct ParticleLod {
5602    pub lod_level: u32,
5603    pub max_distance: f32,
5604    pub particle_count_mult: f32,
5605    pub update_rate_hz: f32,
5606    pub disable_effects: Vec<String>,
5607}
5608
5609impl ParticleLod {
5610    pub fn new(level: u32, max_dist: f32, count_mult: f32, update_rate: f32) -> Self {
5611        Self { lod_level: level, max_distance: max_dist, particle_count_mult: count_mult, update_rate_hz: update_rate, disable_effects: Vec::new() }
5612    }
5613    pub fn disable_effect(mut self, effect: impl Into<String>) -> Self { self.disable_effects.push(effect.into()); self }
5614    pub fn is_active_at_distance(&self, dist: f32) -> bool { dist <= self.max_distance }
5615}
5616
5617#[derive(Clone, Debug)]
5618pub struct ParticleLodController {
5619    pub lods: Vec<ParticleLod>,
5620    pub camera_pos: Vec3,
5621    pub current_lod: u32,
5622}
5623
5624impl ParticleLodController {
5625    pub fn new() -> Self {
5626        let lods = vec![
5627            ParticleLod::new(0, 10.0, 1.0, 60.0),
5628            ParticleLod::new(1, 30.0, 0.5, 30.0),
5629            ParticleLod::new(2, 80.0, 0.2, 15.0),
5630            ParticleLod::new(3, 200.0, 0.05, 5.0),
5631        ];
5632        Self { lods, camera_pos: Vec3::ZERO, current_lod: 0 }
5633    }
5634    pub fn update_lod(&mut self, emitter_pos: Vec3) {
5635        let dist = (emitter_pos - self.camera_pos).length();
5636        self.current_lod = self.lods.iter().enumerate()
5637            .find(|(_, lod)| lod.is_active_at_distance(dist))
5638            .map(|(i, _)| i as u32)
5639            .unwrap_or(self.lods.len() as u32 - 1);
5640    }
5641    pub fn count_multiplier(&self) -> f32 {
5642        self.lods.get(self.current_lod as usize).map(|l| l.particle_count_mult).unwrap_or(0.01)
5643    }
5644    pub fn update_rate(&self) -> f32 {
5645        self.lods.get(self.current_lod as usize).map(|l| l.update_rate_hz).unwrap_or(1.0)
5646    }
5647}
5648
5649impl Default for ParticleLodController {
5650    fn default() -> Self { Self::new() }
5651}
5652
5653// ── Particle Spawner Shapes ───────────────────────────────────────────────────
5654
5655#[derive(Clone, Debug)]
5656pub struct SpawnerShape {
5657    pub shape_type: SpawnerShapeType,
5658    pub scale: Vec3,
5659    pub rotation: Quat,
5660    pub surface_only: bool,
5661    pub randomize_direction: bool,
5662}
5663
5664#[derive(Clone, Debug, PartialEq)]
5665pub enum SpawnerShapeType {
5666    Point,
5667    Sphere,
5668    Hemisphere,
5669    Box,
5670    Circle,
5671    Edge,
5672    Cone,
5673    Mesh,
5674}
5675
5676impl SpawnerShape {
5677    pub fn point() -> Self { Self { shape_type: SpawnerShapeType::Point, scale: Vec3::ONE, rotation: Quat::IDENTITY, surface_only: false, randomize_direction: true } }
5678    pub fn sphere(radius: f32) -> Self { Self { shape_type: SpawnerShapeType::Sphere, scale: Vec3::splat(radius), rotation: Quat::IDENTITY, surface_only: false, randomize_direction: true } }
5679    pub fn cone(angle: f32, height: f32) -> Self { Self { shape_type: SpawnerShapeType::Cone, scale: Vec3::new(angle, height, angle), rotation: Quat::IDENTITY, surface_only: false, randomize_direction: true } }
5680    pub fn bounding_volume(&self) -> f32 { self.scale.x * self.scale.y * self.scale.z }
5681    pub fn is_volumetric(&self) -> bool { !self.surface_only }
5682}
5683
5684// ── Particle System Presets ───────────────────────────────────────────────────
5685
5686#[derive(Clone, Debug)]
5687pub struct ParticlePreset {
5688    pub id: u32,
5689    pub name: String,
5690    pub category: String,
5691    pub description: String,
5692    pub tags: Vec<String>,
5693    pub thumbnail_id: u32,
5694    pub is_builtin: bool,
5695}
5696
5697impl ParticlePreset {
5698    pub fn new(id: u32, name: impl Into<String>, category: impl Into<String>) -> Self {
5699        Self { id, name: name.into(), category: category.into(), description: String::new(), tags: Vec::new(), thumbnail_id: 0, is_builtin: false }
5700    }
5701    pub fn builtin(mut self) -> Self { self.is_builtin = true; self }
5702    pub fn with_tag(mut self, tag: impl Into<String>) -> Self { self.tags.push(tag.into()); self }
5703    pub fn with_description(mut self, desc: impl Into<String>) -> Self { self.description = desc.into(); self }
5704}
5705
5706#[derive(Clone, Debug)]
5707pub struct ParticlePresetLibrary {
5708    pub presets: Vec<ParticlePreset>,
5709    pub favorites: HashSet<u32>,
5710    pub recently_used: VecDeque<u32>,
5711}
5712
5713impl ParticlePresetLibrary {
5714    pub fn new() -> Self { Self { presets: Vec::new(), favorites: HashSet::new(), recently_used: VecDeque::new() } }
5715    pub fn add(&mut self, preset: ParticlePreset) { self.presets.push(preset); }
5716    pub fn find_by_name(&self, name: &str) -> Option<&ParticlePreset> { self.presets.iter().find(|p| p.name == name) }
5717    pub fn find_by_category(&self, cat: &str) -> Vec<&ParticlePreset> { self.presets.iter().filter(|p| p.category == cat).collect() }
5718    pub fn find_by_tag(&self, tag: &str) -> Vec<&ParticlePreset> { self.presets.iter().filter(|p| p.tags.contains(&tag.to_string())).collect() }
5719    pub fn favorite(&mut self, id: u32) { self.favorites.insert(id); }
5720    pub fn unfavorite(&mut self, id: u32) { self.favorites.remove(&id); }
5721    pub fn use_preset(&mut self, id: u32) {
5722        self.recently_used.retain(|&r| r != id);
5723        self.recently_used.push_front(id);
5724        if self.recently_used.len() > 20 { self.recently_used.pop_back(); }
5725    }
5726    pub fn builtin_count(&self) -> usize { self.presets.iter().filter(|p| p.is_builtin).count() }
5727    pub fn total(&self) -> usize { self.presets.len() }
5728}
5729
5730impl Default for ParticlePresetLibrary {
5731    fn default() -> Self { Self::new() }
5732}
5733
5734// ── Particle Timeline Keyframe System ────────────────────────────────────────
5735
5736#[derive(Clone, Debug)]
5737pub struct ParticleKeyframe<T: Clone> {
5738    pub time: f32,
5739    pub value: T,
5740    pub interpolation: InterpolationType,
5741}
5742
5743#[derive(Clone, Debug, PartialEq)]
5744pub enum InterpolationType { Linear, Step, Smooth, Cubic }
5745
5746impl<T: Clone> ParticleKeyframe<T> {
5747    pub fn new(time: f32, value: T) -> Self { Self { time, value, interpolation: InterpolationType::Linear } }
5748    pub fn stepped(time: f32, value: T) -> Self { Self { time, value, interpolation: InterpolationType::Step } }
5749}
5750
5751#[derive(Clone, Debug)]
5752pub struct FloatCurveEx {
5753    pub keyframes: Vec<ParticleKeyframe<f32>>,
5754    pub name: String,
5755}
5756
5757impl FloatCurveEx {
5758    pub fn new(name: impl Into<String>) -> Self { Self { keyframes: Vec::new(), name: name.into() } }
5759    pub fn constant(name: impl Into<String>, value: f32) -> Self {
5760        let mut c = Self::new(name); c.add_key(0.0, value); c
5761    }
5762    pub fn add_key(&mut self, time: f32, value: f32) { self.keyframes.push(ParticleKeyframe::new(time, value)); self.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal)); }
5763    pub fn evaluate(&self, t: f32) -> f32 {
5764        if self.keyframes.is_empty() { return 0.0; }
5765        if t <= self.keyframes[0].time { return self.keyframes[0].value; }
5766        let last = self.keyframes.last().unwrap();
5767        if t >= last.time { return last.value; }
5768        let idx = self.keyframes.partition_point(|k| k.time <= t) - 1;
5769        let a = &self.keyframes[idx];
5770        let b = &self.keyframes[idx + 1];
5771        let alpha = (t - a.time) / (b.time - a.time);
5772        match a.interpolation {
5773            InterpolationType::Step => a.value,
5774            InterpolationType::Smooth => { let s = alpha * alpha * (3.0 - 2.0 * alpha); a.value + (b.value - a.value) * s }
5775            _ => a.value + (b.value - a.value) * alpha,
5776        }
5777    }
5778    pub fn key_count(&self) -> usize { self.keyframes.len() }
5779    pub fn duration(&self) -> f32 { self.keyframes.last().map(|k| k.time).unwrap_or(0.0) }
5780}
5781
5782// ── Particle Statistics ───────────────────────────────────────────────────────
5783
5784#[derive(Clone, Debug, Default)]
5785pub struct ParticleStats {
5786    pub active_particles: u32,
5787    pub particles_spawned_this_frame: u32,
5788    pub particles_killed_this_frame: u32,
5789    pub active_emitters: u32,
5790    pub culled_emitters: u32,
5791    pub draw_calls: u32,
5792    pub vertices_rendered: u32,
5793    pub simulation_time_ms: f32,
5794    pub render_time_ms: f32,
5795    pub peak_particles: u32,
5796    pub frame_count: u64,
5797}
5798
5799impl ParticleStats {
5800    pub fn new() -> Self { Self::default() }
5801    pub fn begin_frame(&mut self) {
5802        self.particles_spawned_this_frame = 0;
5803        self.particles_killed_this_frame = 0;
5804        self.draw_calls = 0;
5805        self.vertices_rendered = 0;
5806        self.frame_count += 1;
5807    }
5808    pub fn record_spawn(&mut self, n: u32) { self.particles_spawned_this_frame += n; self.active_particles += n; if self.active_particles > self.peak_particles { self.peak_particles = self.active_particles; } }
5809    pub fn record_kill(&mut self, n: u32) { self.particles_killed_this_frame += n; self.active_particles = self.active_particles.saturating_sub(n); }
5810    pub fn record_draw_call(&mut self, verts: u32) { self.draw_calls += 1; self.vertices_rendered += verts; }
5811    pub fn cpu_ms_total(&self) -> f32 { self.simulation_time_ms + self.render_time_ms }
5812}
5813
5814// ── Particle System Constants ─────────────────────────────────────────────────
5815
5816pub const PARTICLE_MAX_EMITTERS: usize = 256;
5817pub const PARTICLE_MAX_PER_EMITTER: u32 = 50000;
5818pub const PARTICLE_MAX_TOTAL: u32 = 1_000_000;
5819pub const PARTICLE_MAX_ATTRACTORS: usize = 32;
5820pub const PARTICLE_MAX_COLLIDERS: usize = 64;
5821pub const PARTICLE_LOD_LEVELS: usize = 4;
5822pub const PARTICLE_TRAIL_MAX_LENGTH: usize = 256;
5823pub const PARTICLE_CURVE_MAX_KEYS: usize = 64;
5824pub const PARTICLE_PRESET_BUILTIN_COUNT: usize = 32;
5825pub const PARTICLE_MAX_FORCE_FIELDS: usize = 16;
5826
5827pub fn particle_system_info() -> &'static str {
5828    "ParticleSystem v2.0 — emitters, trails, LOD, attractors, force fields, presets, curves"
5829}
5830
5831
5832// ── Particle Renderer ─────────────────────────────────────────────────────────
5833
5834#[derive(Clone, Debug)]
5835pub struct ParticleRenderBatch {
5836    pub emitter_id: u32,
5837    pub material_id: u32,
5838    pub blend_mode: BlendMode,
5839    pub positions: Vec<Vec3>,
5840    pub colors: Vec<Vec4>,
5841    pub sizes: Vec<f32>,
5842    pub rotations: Vec<f32>,
5843    pub particle_count: u32,
5844    pub sort_key: f32,
5845}
5846
5847#[derive(Clone, Debug, PartialEq)]
5848pub enum BlendMode { Additive, Alpha, Multiply, Screen, Premultiplied }
5849
5850impl ParticleRenderBatch {
5851    pub fn new(emitter_id: u32, material_id: u32) -> Self {
5852        Self { emitter_id, material_id, blend_mode: BlendMode::Alpha, positions: Vec::new(), colors: Vec::new(), sizes: Vec::new(), rotations: Vec::new(), particle_count: 0, sort_key: 0.0 }
5853    }
5854    pub fn add_particle(&mut self, pos: Vec3, color: Vec4, size: f32, rotation: f32) {
5855        self.positions.push(pos); self.colors.push(color); self.sizes.push(size); self.rotations.push(rotation);
5856        self.particle_count += 1;
5857    }
5858    pub fn clear(&mut self) { self.positions.clear(); self.colors.clear(); self.sizes.clear(); self.rotations.clear(); self.particle_count = 0; }
5859    pub fn is_empty(&self) -> bool { self.particle_count == 0 }
5860    pub fn vertex_count(&self) -> u32 { self.particle_count * 4 }
5861    pub fn index_count(&self) -> u32 { self.particle_count * 6 }
5862}
5863
5864#[derive(Clone, Debug)]
5865pub struct ParticleRendererEx {
5866    pub batches: Vec<ParticleRenderBatch>,
5867    pub sort_transparent: bool,
5868    pub camera_pos: Vec3,
5869    pub camera_forward: Vec3,
5870    pub draw_call_count: u32,
5871}
5872
5873impl ParticleRendererEx {
5874    pub fn new() -> Self { Self { batches: Vec::new(), sort_transparent: true, camera_pos: Vec3::ZERO, camera_forward: Vec3::NEG_Z, draw_call_count: 0 } }
5875    pub fn add_batch(&mut self, batch: ParticleRenderBatch) { self.batches.push(batch); }
5876    pub fn clear(&mut self) { self.batches.clear(); self.draw_call_count = 0; }
5877    pub fn sort_batches(&mut self) {
5878        if self.sort_transparent { self.batches.sort_by(|a, b| b.sort_key.partial_cmp(&a.sort_key).unwrap_or(std::cmp::Ordering::Equal)); }
5879    }
5880    pub fn total_particles(&self) -> u32 { self.batches.iter().map(|b| b.particle_count).sum() }
5881    pub fn batch_count(&self) -> usize { self.batches.len() }
5882    pub fn set_camera(&mut self, pos: Vec3, forward: Vec3) { self.camera_pos = pos; self.camera_forward = forward; }
5883}
5884
5885impl Default for ParticleRendererEx {
5886    fn default() -> Self { Self::new() }
5887}
5888
5889// ── Particle Spawner Pool ─────────────────────────────────────────────────────
5890
5891#[derive(Clone, Debug)]
5892pub struct ParticlePoolEx {
5893    pub capacity: u32,
5894    pub free_indices: Vec<u32>,
5895    pub used_count: u32,
5896    pub peak_used: u32,
5897    pub recycle_count: u64,
5898}
5899
5900impl ParticlePoolEx {
5901    pub fn new(capacity: u32) -> Self {
5902        let free_indices = (0..capacity).rev().collect();
5903        Self { capacity, free_indices, used_count: 0, peak_used: 0, recycle_count: 0 }
5904    }
5905    pub fn allocate(&mut self) -> Option<u32> {
5906        let idx = self.free_indices.pop()?;
5907        self.used_count += 1;
5908        if self.used_count > self.peak_used { self.peak_used = self.used_count; }
5909        Some(idx)
5910    }
5911    pub fn free(&mut self, idx: u32) {
5912        if idx < self.capacity { self.free_indices.push(idx); self.used_count = self.used_count.saturating_sub(1); self.recycle_count += 1; }
5913    }
5914    pub fn available(&self) -> u32 { self.free_indices.len() as u32 }
5915    pub fn is_full(&self) -> bool { self.free_indices.is_empty() }
5916    pub fn utilization(&self) -> f32 { if self.capacity == 0 { 0.0 } else { self.used_count as f32 / self.capacity as f32 } }
5917    pub fn reset(&mut self) { self.free_indices = (0..self.capacity).rev().collect(); self.used_count = 0; }
5918}
5919
5920// ── Particle Effect Asset ─────────────────────────────────────────────────────
5921
5922#[derive(Clone, Debug)]
5923pub struct ParticleEffectAssetEx {
5924    pub id: u32,
5925    pub name: String,
5926    pub file_path: String,
5927    pub version: u32,
5928    pub author: String,
5929    pub tags: Vec<String>,
5930    pub duration_secs: f32,
5931    pub is_looping: bool,
5932    pub peak_particle_count: u32,
5933    pub texture_ids: Vec<u32>,
5934    pub emitter_count: u32,
5935    pub created_at: u64,
5936    pub modified_at: u64,
5937}
5938
5939impl ParticleEffectAssetEx {
5940    pub fn new(id: u32, name: impl Into<String>) -> Self {
5941        Self { id, name: name.into(), file_path: String::new(), version: 1, author: String::new(), tags: Vec::new(), duration_secs: 2.0, is_looping: false, peak_particle_count: 0, texture_ids: Vec::new(), emitter_count: 0, created_at: 0, modified_at: 0 }
5942    }
5943    pub fn with_path(mut self, path: impl Into<String>) -> Self { self.file_path = path.into(); self }
5944    pub fn looping(mut self) -> Self { self.is_looping = true; self }
5945    pub fn add_tag(&mut self, tag: impl Into<String>) { self.tags.push(tag.into()); }
5946    pub fn add_texture(&mut self, id: u32) { self.texture_ids.push(id); }
5947    pub fn is_short(&self) -> bool { self.duration_secs < 1.0 }
5948    pub fn is_long(&self) -> bool { self.duration_secs > 10.0 }
5949}
5950
5951#[derive(Clone, Debug)]
5952pub struct ParticleAssetLibrary {
5953    pub assets: HashMap<u32, ParticleEffectAssetEx>,
5954    pub next_id: u32,
5955    pub search_index: HashMap<String, Vec<u32>>,
5956}
5957
5958impl ParticleAssetLibrary {
5959    pub fn new() -> Self { Self { assets: HashMap::new(), next_id: 1, search_index: HashMap::new() } }
5960    pub fn add(&mut self, mut asset: ParticleEffectAssetEx) -> u32 {
5961        let id = self.next_id; self.next_id += 1;
5962        asset.id = id;
5963        for tag in &asset.tags { self.search_index.entry(tag.clone()).or_default().push(id); }
5964        self.assets.insert(id, asset);
5965        id
5966    }
5967    pub fn get(&self, id: u32) -> Option<&ParticleEffectAssetEx> { self.assets.get(&id) }
5968    pub fn find_by_tag(&self, tag: &str) -> Vec<&ParticleEffectAssetEx> {
5969        self.search_index.get(tag).map(|ids| ids.iter().filter_map(|id| self.assets.get(id)).collect()).unwrap_or_default()
5970    }
5971    pub fn find_by_name(&self, name: &str) -> Option<&ParticleEffectAssetEx> { self.assets.values().find(|a| a.name == name) }
5972    pub fn count(&self) -> usize { self.assets.len() }
5973}
5974
5975impl Default for ParticleAssetLibrary {
5976    fn default() -> Self { Self::new() }
5977}
5978
5979// ── Particle Simulation State ─────────────────────────────────────────────────
5980
5981#[derive(Clone, Debug)]
5982pub struct ParticleSimState {
5983    pub time: f32,
5984    pub delta_time: f32,
5985    pub frame: u64,
5986    pub paused: bool,
5987    pub time_scale: f32,
5988    pub gravity: Vec3,
5989    pub wind: Vec3,
5990    pub random_seed: u64,
5991}
5992
5993impl ParticleSimState {
5994    pub fn new() -> Self {
5995        Self { time: 0.0, delta_time: 0.016, frame: 0, paused: false, time_scale: 1.0, gravity: Vec3::new(0.0, -9.81, 0.0), wind: Vec3::ZERO, random_seed: 42 }
5996    }
5997    pub fn tick(&mut self, dt: f32) {
5998        if self.paused { return; }
5999        let scaled_dt = dt * self.time_scale;
6000        self.time += scaled_dt;
6001        self.delta_time = scaled_dt;
6002        self.frame += 1;
6003    }
6004    pub fn pause(&mut self) { self.paused = true; }
6005    pub fn resume(&mut self) { self.paused = false; }
6006    pub fn set_time_scale(&mut self, scale: f32) { self.time_scale = scale.max(0.0); }
6007    pub fn effective_gravity(&self) -> Vec3 { self.gravity + self.wind * 0.1 }
6008    pub fn is_running(&self) -> bool { !self.paused }
6009}
6010
6011impl Default for ParticleSimState {
6012    fn default() -> Self { Self::new() }
6013}
6014
6015// ── Particle Debug Visualizer ─────────────────────────────────────────────────
6016
6017#[derive(Clone, Debug, Default)]
6018pub struct ParticleDebugSettings {
6019    pub show_bounds: bool,
6020    pub show_velocity: bool,
6021    pub show_force_fields: bool,
6022    pub show_attractors: bool,
6023    pub show_colliders: bool,
6024    pub show_trail_points: bool,
6025    pub show_lod_regions: bool,
6026    pub show_stats_overlay: bool,
6027    pub highlight_emitter_id: Option<u32>,
6028    pub velocity_scale: f32,
6029}
6030
6031impl ParticleDebugSettings {
6032    pub fn new() -> Self { Self { velocity_scale: 0.1, ..Default::default() } }
6033    pub fn show_all(mut self) -> Self {
6034        self.show_bounds = true; self.show_velocity = true; self.show_force_fields = true;
6035        self.show_attractors = true; self.show_colliders = true; self.show_trail_points = true;
6036        self.show_lod_regions = true; self.show_stats_overlay = true;
6037        self
6038    }
6039    pub fn hide_all(mut self) -> Self {
6040        self.show_bounds = false; self.show_velocity = false; self.show_force_fields = false;
6041        self.show_attractors = false; self.show_colliders = false; self.show_trail_points = false;
6042        self.show_lod_regions = false; self.show_stats_overlay = false;
6043        self
6044    }
6045    pub fn any_debug_active(&self) -> bool {
6046        self.show_bounds || self.show_velocity || self.show_force_fields || self.show_attractors
6047    }
6048}
6049
6050// ── Particle System Manager ───────────────────────────────────────────────────
6051
6052#[derive(Clone, Debug)]
6053pub struct ParticleSystemManager {
6054    pub active_effects: HashMap<u32, u32>,
6055    pub pool: ParticlePoolEx,
6056    pub sim_state: ParticleSimState,
6057    pub stats: ParticleStats,
6058    pub debug: ParticleDebugSettings,
6059    pub lod: ParticleLodController,
6060    pub renderer: ParticleRendererEx,
6061    pub asset_library: ParticleAssetLibrary,
6062    pub next_effect_instance: u32,
6063    pub max_concurrent_effects: u32,
6064}
6065
6066impl ParticleSystemManager {
6067    pub fn new(pool_size: u32) -> Self {
6068        Self {
6069            active_effects: HashMap::new(),
6070            pool: ParticlePoolEx::new(pool_size),
6071            sim_state: ParticleSimState::new(),
6072            stats: ParticleStats::new(),
6073            debug: ParticleDebugSettings::new(),
6074            lod: ParticleLodController::new(),
6075            renderer: ParticleRendererEx::new(),
6076            asset_library: ParticleAssetLibrary::new(),
6077            next_effect_instance: 1,
6078            max_concurrent_effects: 64,
6079        }
6080    }
6081    pub fn spawn_effect(&mut self, asset_id: u32, _position: Vec3) -> Option<u32> {
6082        if self.active_effects.len() >= self.max_concurrent_effects as usize { return None; }
6083        let instance_id = self.next_effect_instance; self.next_effect_instance += 1;
6084        self.active_effects.insert(instance_id, asset_id);
6085        Some(instance_id)
6086    }
6087    pub fn kill_effect(&mut self, instance_id: u32) -> bool { self.active_effects.remove(&instance_id).is_some() }
6088    pub fn kill_all(&mut self) { self.active_effects.clear(); }
6089    pub fn tick(&mut self, dt: f32) { self.sim_state.tick(dt); self.stats.begin_frame(); }
6090    pub fn active_count(&self) -> usize { self.active_effects.len() }
6091    pub fn pause(&mut self) { self.sim_state.pause(); }
6092    pub fn resume(&mut self) { self.sim_state.resume(); }
6093    pub fn pool_utilization(&self) -> f32 { self.pool.utilization() }
6094    pub fn is_at_capacity(&self) -> bool { self.active_effects.len() >= self.max_concurrent_effects as usize }
6095}
6096
6097impl Default for ParticleSystemManager {
6098    fn default() -> Self { Self::new(100_000) }
6099}
6100
6101// ── Additional helpers ────────────────────────────────────────────────────────
6102
6103pub fn lerp_color(a: Vec4, b: Vec4, t: f32) -> Vec4 { a + (b - a) * t.clamp(0.0, 1.0) }
6104pub fn lerp_size(start: f32, end: f32, t: f32) -> f32 { start + (end - start) * t.clamp(0.0, 1.0) }
6105pub fn fade_in_out(t: f32, fade_in: f32, fade_out: f32) -> f32 {
6106    if t < fade_in { t / fade_in.max(1e-5) }
6107    else if t > 1.0 - fade_out { (1.0 - t) / fade_out.max(1e-5) }
6108    else { 1.0 }
6109}
6110pub fn billboard_matrix(pos: Vec3, cam_pos: Vec3, up: Vec3) -> Mat4 {
6111    let forward = (cam_pos - pos).normalize();
6112    let right = up.cross(forward).normalize();
6113    let actual_up = forward.cross(right);
6114    Mat4::from_cols(right.extend(0.0), actual_up.extend(0.0), forward.extend(0.0), pos.extend(1.0))
6115}
6116pub fn velocity_from_angle(angle_deg: f32, speed: f32) -> Vec3 {
6117    let rad = angle_deg.to_radians();
6118    Vec3::new(rad.cos() * speed, 0.0, rad.sin() * speed)
6119}
6120pub fn random_on_sphere(u: f32, v: f32) -> Vec3 {
6121    let theta = 2.0 * std::f32::consts::PI * u;
6122    let phi = (1.0 - 2.0 * v).acos();
6123    Vec3::new(phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin())
6124}
6125pub fn random_in_sphere(u: f32, v: f32, w: f32) -> Vec3 {
6126    random_on_sphere(u, v) * w.cbrt()
6127}
6128pub fn particle_system_default_gravity() -> Vec3 { Vec3::new(0.0, -9.81, 0.0) }
6129pub fn blend_mode_name(mode: &BlendMode) -> &'static str {
6130    match mode { BlendMode::Additive => "Additive", BlendMode::Alpha => "Alpha", BlendMode::Multiply => "Multiply", BlendMode::Screen => "Screen", BlendMode::Premultiplied => "Premultiplied" }
6131}
6132
6133
6134// ── Particle Noise Field ──────────────────────────────────────────────────────
6135
6136#[derive(Clone, Debug)]
6137pub struct NoiseField {
6138    pub frequency: f32,
6139    pub amplitude: f32,
6140    pub octaves: u32,
6141    pub lacunarity: f32,
6142    pub persistence: f32,
6143    pub offset: Vec3,
6144    pub scroll_speed: Vec3,
6145    pub enabled: bool,
6146}
6147
6148impl NoiseField {
6149    pub fn new(frequency: f32, amplitude: f32) -> Self {
6150        Self { frequency, amplitude, octaves: 4, lacunarity: 2.0, persistence: 0.5, offset: Vec3::ZERO, scroll_speed: Vec3::ZERO, enabled: true }
6151    }
6152    pub fn sample(&self, pos: Vec3, time: f32) -> Vec3 {
6153        let p = pos * self.frequency + self.offset + self.scroll_speed * time;
6154        // Simple pseudo-noise using sin waves
6155        let nx = (p.x * 1.1 + p.y * 0.7 + p.z * 0.3).sin() * self.amplitude;
6156        let ny = (p.x * 0.3 + p.y * 1.3 + p.z * 0.9).sin() * self.amplitude;
6157        let nz = (p.x * 0.7 + p.y * 0.5 + p.z * 1.1).sin() * self.amplitude;
6158        Vec3::new(nx, ny, nz)
6159    }
6160    pub fn scroll(&mut self, dt: f32) { self.offset += self.scroll_speed * dt; }
6161    pub fn set_turbulence(mut self, octaves: u32) -> Self { self.octaves = octaves; self }
6162}
6163
6164impl Default for NoiseField {
6165    fn default() -> Self { Self::new(0.5, 1.0) }
6166}
6167
6168// ── Particle Spawn Burst ──────────────────────────────────────────────────────
6169
6170#[derive(Clone, Debug)]
6171pub struct SpawnBurst {
6172    pub time: f32,
6173    pub count_min: u32,
6174    pub count_max: u32,
6175    pub probability: f32,
6176    pub triggered: bool,
6177    pub cycles: u32,
6178    pub cycle_interval: f32,
6179    pub cycles_done: u32,
6180}
6181
6182impl SpawnBurst {
6183    pub fn new(time: f32, count: u32) -> Self {
6184        Self { time, count_min: count, count_max: count, probability: 1.0, triggered: false, cycles: 1, cycle_interval: 0.0, cycles_done: 0 }
6185    }
6186    pub fn range(mut self, min: u32, max: u32) -> Self { self.count_min = min; self.count_max = max; self }
6187    pub fn repeating(mut self, cycles: u32, interval: f32) -> Self { self.cycles = cycles; self.cycle_interval = interval; self }
6188    pub fn should_trigger(&self, current_time: f32) -> bool {
6189        !self.triggered && self.cycles_done < self.cycles &&
6190        current_time >= self.time + self.cycles_done as f32 * self.cycle_interval
6191    }
6192    pub fn trigger(&mut self) {
6193        self.triggered = self.cycles_done + 1 >= self.cycles;
6194        self.cycles_done += 1;
6195    }
6196    pub fn is_done(&self) -> bool { self.cycles_done >= self.cycles }
6197}
6198
6199// ── Particle Sub-Emitter ──────────────────────────────────────────────────────
6200
6201#[derive(Clone, Debug, PartialEq)]
6202pub enum SubEmitterEvent { Birth, Death, Collision, Manual }
6203
6204#[derive(Clone, Debug)]
6205pub struct SubEmitter {
6206    pub id: u32,
6207    pub trigger_event: SubEmitterEvent,
6208    pub emitter_asset_id: u32,
6209    pub inherit_velocity: bool,
6210    pub inherit_color: bool,
6211    pub inherit_size: f32,
6212    pub probability: f32,
6213    pub cooldown: f32,
6214    pub last_triggered: f32,
6215}
6216
6217impl SubEmitter {
6218    pub fn new(id: u32, event: SubEmitterEvent, asset_id: u32) -> Self {
6219        Self { id, trigger_event: event, emitter_asset_id: asset_id, inherit_velocity: true, inherit_color: false, inherit_size: 1.0, probability: 1.0, cooldown: 0.0, last_triggered: -999.0 }
6220    }
6221    pub fn can_trigger(&self, time: f32, roll: f32) -> bool {
6222        time - self.last_triggered >= self.cooldown && roll <= self.probability
6223    }
6224    pub fn record_trigger(&mut self, time: f32) { self.last_triggered = time; }
6225}
6226
6227// ── Particle Texture Animation ────────────────────────────────────────────────
6228
6229#[derive(Clone, Debug)]
6230pub struct TextureSheetAnimation {
6231    pub columns: u32,
6232    pub rows: u32,
6233    pub frame_count: u32,
6234    pub animation_speed: f32,
6235    pub loop_animation: bool,
6236    pub start_frame: u32,
6237    pub end_frame: u32,
6238    pub random_start_frame: bool,
6239}
6240
6241impl TextureSheetAnimation {
6242    pub fn new(columns: u32, rows: u32) -> Self {
6243        let total = columns * rows;
6244        Self { columns, rows, frame_count: total, animation_speed: 30.0, loop_animation: true, start_frame: 0, end_frame: total.saturating_sub(1), random_start_frame: false }
6245    }
6246    pub fn frame_at_time(&self, time: f32, lifetime: f32) -> u32 {
6247        if self.frame_count == 0 { return 0; }
6248        let t = if lifetime > 0.0 { time / lifetime } else { time * self.animation_speed / self.frame_count as f32 };
6249        let t = if self.loop_animation { t.fract() } else { t.clamp(0.0, 1.0) };
6250        let range = self.end_frame - self.start_frame + 1;
6251        self.start_frame + (t * range as f32) as u32 % range
6252    }
6253    pub fn uv_for_frame(&self, frame: u32) -> (f32, f32, f32, f32) {
6254        let frame = frame.min(self.frame_count.saturating_sub(1));
6255        let col = frame % self.columns;
6256        let row = frame / self.columns;
6257        let w = 1.0 / self.columns as f32;
6258        let h = 1.0 / self.rows as f32;
6259        (col as f32 * w, row as f32 * h, w, h)
6260    }
6261    pub fn total_frames(&self) -> u32 { self.frame_count }
6262    pub fn duration(&self) -> f32 { self.frame_count as f32 / self.animation_speed.max(1.0) }
6263}
6264
6265impl Default for TextureSheetAnimation {
6266    fn default() -> Self { Self::new(1, 1) }
6267}
6268
6269// ── Particle Color Over Lifetime ──────────────────────────────────────────────
6270
6271#[derive(Clone, Debug)]
6272pub struct ColorOverLifetime {
6273    pub gradient: Vec<(f32, Vec4)>,
6274    pub mode: ColorMode,
6275}
6276
6277#[derive(Clone, Debug, PartialEq)]
6278pub enum ColorMode { Single, Gradient, RandomBetweenTwo, RandomColor }
6279
6280impl ColorOverLifetime {
6281    pub fn constant(color: Vec4) -> Self { Self { gradient: vec![(0.0, color), (1.0, color)], mode: ColorMode::Single } }
6282    pub fn gradient(colors: Vec<(f32, Vec4)>) -> Self { Self { gradient: colors, mode: ColorMode::Gradient } }
6283    pub fn fade_out(color: Vec4) -> Self {
6284        let transparent = Vec4::new(color.x, color.y, color.z, 0.0);
6285        Self { gradient: vec![(0.0, color), (1.0, transparent)], mode: ColorMode::Gradient }
6286    }
6287    pub fn evaluate(&self, t: f32) -> Vec4 {
6288        if self.gradient.is_empty() { return Vec4::ONE; }
6289        if self.gradient.len() == 1 { return self.gradient[0].1; }
6290        let t = t.clamp(0.0, 1.0);
6291        let idx = self.gradient.partition_point(|(time, _)| *time <= t).saturating_sub(1);
6292        if idx + 1 >= self.gradient.len() { return self.gradient.last().unwrap().1; }
6293        let (t0, c0) = self.gradient[idx];
6294        let (t1, c1) = self.gradient[idx + 1];
6295        let alpha = if (t1 - t0).abs() < 1e-6 { 0.0 } else { (t - t0) / (t1 - t0) };
6296        lerp_color(c0, c1, alpha)
6297    }
6298    pub fn add_stop(&mut self, time: f32, color: Vec4) {
6299        self.gradient.push((time, color));
6300        self.gradient.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
6301    }
6302}
6303
6304// ── Particle Size Over Lifetime ───────────────────────────────────────────────
6305
6306#[derive(Clone, Debug)]
6307pub struct SizeOverLifetime {
6308    pub curve: FloatCurveEx,
6309    pub base_size: f32,
6310}
6311
6312impl SizeOverLifetime {
6313    pub fn constant(size: f32) -> Self { Self { curve: FloatCurveEx::constant("size", 1.0), base_size: size } }
6314    pub fn shrink(start: f32, end: f32) -> Self {
6315        let mut curve = FloatCurveEx::new("size");
6316        curve.add_key(0.0, start / start.max(1e-5));
6317        curve.add_key(1.0, end / start.max(1e-5));
6318        Self { curve, base_size: start }
6319    }
6320    pub fn evaluate(&self, t: f32) -> f32 { self.base_size * self.curve.evaluate(t) }
6321}
6322
6323// ── Velocity Over Lifetime ────────────────────────────────────────────────────
6324
6325#[derive(Clone, Debug)]
6326pub struct VelocityOverLifetime {
6327    pub x_curve: FloatCurveEx,
6328    pub y_curve: FloatCurveEx,
6329    pub z_curve: FloatCurveEx,
6330    pub space: VelocitySpace,
6331    pub speed_modifier: FloatCurveEx,
6332}
6333
6334#[derive(Clone, Debug, PartialEq)]
6335pub enum VelocitySpace { Local, World }
6336
6337impl VelocityOverLifetime {
6338    pub fn constant(vel: Vec3) -> Self {
6339        Self {
6340            x_curve: FloatCurveEx::constant("vx", vel.x),
6341            y_curve: FloatCurveEx::constant("vy", vel.y),
6342            z_curve: FloatCurveEx::constant("vz", vel.z),
6343            space: VelocitySpace::World,
6344            speed_modifier: FloatCurveEx::constant("speed", 1.0),
6345        }
6346    }
6347    pub fn evaluate(&self, t: f32) -> Vec3 {
6348        let speed = self.speed_modifier.evaluate(t);
6349        Vec3::new(self.x_curve.evaluate(t), self.y_curve.evaluate(t), self.z_curve.evaluate(t)) * speed
6350    }
6351    pub fn zero() -> Self { Self::constant(Vec3::ZERO) }
6352}
6353
6354// ── Extended Particle Constants ───────────────────────────────────────────────
6355
6356pub const PARTICLE_MAX_BURST_EVENTS: usize = 8;
6357pub const PARTICLE_MAX_SUB_EMITTERS: usize = 4;
6358pub const PARTICLE_TEXTURE_SHEET_MAX_FRAMES: u32 = 256;
6359pub const PARTICLE_COLOR_GRADIENT_MAX_STOPS: usize = 8;
6360pub const PARTICLE_NOISE_OCTAVES_MAX: u32 = 8;
6361pub const PARTICLE_MAX_TRAIL_EMITTERS: usize = 16;
6362pub const PARTICLE_RENDERER_MAX_BATCHES: usize = 512;
6363pub const PARTICLE_POOL_OVERCOMMIT: f32 = 0.1;
6364pub const PARTICLE_ASSET_LIBRARY_MAX: usize = 1024;
6365pub const PARTICLE_SIMULATION_STEP_MAX: f32 = 0.033;
6366
6367pub fn particle_feature_list() -> &'static [&'static str] {
6368    &[
6369        "emitters", "trails", "attractors", "force_fields",
6370        "colliders", "lod", "presets", "curves", "noise",
6371        "bursts", "sub_emitters", "texture_animation",
6372        "color_lifetime", "size_lifetime", "velocity_lifetime",
6373        "renderer", "pool", "assets", "sim_state", "debug",
6374        "statistics", "spawner_shapes",
6375    ]
6376}
6377
6378pub fn particle_module_count() -> usize { particle_feature_list().len() }
6379pub fn particle_system_full_info() -> String {
6380    format!("ParticleSystemEditor v2.0 — {} modules, max {} total particles", particle_module_count(), PARTICLE_MAX_TOTAL)
6381}
6382
6383
6384// ── Particle Editor UI State ──────────────────────────────────────────────────
6385
6386#[derive(Clone, Debug)]
6387pub struct ParticleEditorState {
6388    pub selected_emitter: Option<u32>,
6389    pub selected_effect: Option<u32>,
6390    pub viewport_camera_pos: Vec3,
6391    pub viewport_camera_rot: Quat,
6392    pub preview_playing: bool,
6393    pub preview_time: f32,
6394    pub show_grid: bool,
6395    pub grid_size: f32,
6396    pub background_color: Vec4,
6397    pub zoom_level: f32,
6398    pub panel_sizes: HashMap<String, f32>,
6399    pub undo_stack: VecDeque<String>,
6400    pub redo_stack: Vec<String>,
6401    pub max_undo: usize,
6402    pub modified: bool,
6403}
6404
6405impl ParticleEditorState {
6406    pub fn new() -> Self {
6407        Self {
6408            selected_emitter: None, selected_effect: None,
6409            viewport_camera_pos: Vec3::new(0.0, 2.0, 5.0),
6410            viewport_camera_rot: Quat::IDENTITY,
6411            preview_playing: false, preview_time: 0.0,
6412            show_grid: true, grid_size: 1.0,
6413            background_color: Vec4::new(0.1, 0.1, 0.15, 1.0),
6414            zoom_level: 1.0,
6415            panel_sizes: HashMap::new(),
6416            undo_stack: VecDeque::new(), redo_stack: Vec::new(),
6417            max_undo: 100, modified: false,
6418        }
6419    }
6420    pub fn play(&mut self) { self.preview_playing = true; }
6421    pub fn stop(&mut self) { self.preview_playing = false; self.preview_time = 0.0; }
6422    pub fn pause(&mut self) { self.preview_playing = false; }
6423    pub fn tick_preview(&mut self, dt: f32) { if self.preview_playing { self.preview_time += dt; } }
6424    pub fn select_emitter(&mut self, id: u32) { self.selected_emitter = Some(id); }
6425    pub fn deselect(&mut self) { self.selected_emitter = None; }
6426    pub fn push_undo(&mut self, desc: impl Into<String>) {
6427        if self.undo_stack.len() >= self.max_undo { self.undo_stack.pop_front(); }
6428        self.undo_stack.push_back(desc.into()); self.redo_stack.clear(); self.modified = true;
6429    }
6430    pub fn undo(&mut self) -> Option<String> { let v = self.undo_stack.pop_back()?; self.redo_stack.push(v.clone()); Some(v) }
6431    pub fn redo(&mut self) -> Option<String> { let v = self.redo_stack.pop()?; self.undo_stack.push_back(v.clone()); Some(v) }
6432    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
6433    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
6434    pub fn mark_saved(&mut self) { self.modified = false; }
6435}
6436
6437impl Default for ParticleEditorState {
6438    fn default() -> Self { Self::new() }
6439}
6440
6441// ── Particle Effect Exporter ──────────────────────────────────────────────────
6442
6443#[derive(Clone, Debug)]
6444pub struct ParticleExportSettings {
6445    pub format: String,
6446    pub include_textures: bool,
6447    pub compress: bool,
6448    pub min_emit_rate: Option<f32>,
6449    pub bake_curves: bool,
6450    pub target_platform: String,
6451    pub output_dir: String,
6452}
6453
6454impl ParticleExportSettings {
6455    pub fn new(format: impl Into<String>, output_dir: impl Into<String>) -> Self {
6456        Self { format: format.into(), include_textures: true, compress: false, min_emit_rate: None, bake_curves: false, target_platform: "pc".into(), output_dir: output_dir.into() }
6457    }
6458    pub fn compressed(mut self) -> Self { self.compress = true; self }
6459    pub fn baked(mut self) -> Self { self.bake_curves = true; self }
6460    pub fn for_platform(mut self, platform: impl Into<String>) -> Self { self.target_platform = platform.into(); self }
6461}
6462
6463#[derive(Clone, Debug)]
6464pub struct ParticleExportResult {
6465    pub success: bool,
6466    pub files_written: Vec<String>,
6467    pub errors: Vec<String>,
6468    pub warnings: Vec<String>,
6469    pub total_bytes: u64,
6470    pub duration_ms: f32,
6471}
6472
6473impl ParticleExportResult {
6474    pub fn ok(files: Vec<String>, bytes: u64) -> Self {
6475        Self { success: true, files_written: files, errors: Vec::new(), warnings: Vec::new(), total_bytes: bytes, duration_ms: 0.0 }
6476    }
6477    pub fn err(msg: impl Into<String>) -> Self {
6478        Self { success: false, files_written: Vec::new(), errors: vec![msg.into()], warnings: Vec::new(), total_bytes: 0, duration_ms: 0.0 }
6479    }
6480    pub fn add_warning(&mut self, w: impl Into<String>) { self.warnings.push(w.into()); }
6481    pub fn file_count(&self) -> usize { self.files_written.len() }
6482}
6483
6484// ── Particle System Benchmark ─────────────────────────────────────────────────
6485
6486#[derive(Clone, Debug, Default)]
6487pub struct ParticleBenchmarkResult {
6488    pub scenario_name: String,
6489    pub particle_count: u32,
6490    pub emitter_count: u32,
6491    pub avg_fps: f32,
6492    pub min_fps: f32,
6493    pub max_fps: f32,
6494    pub avg_sim_ms: f32,
6495    pub avg_render_ms: f32,
6496    pub frame_count: u64,
6497}
6498
6499impl ParticleBenchmarkResult {
6500    pub fn new(name: impl Into<String>) -> Self { Self { scenario_name: name.into(), min_fps: f32::MAX, ..Default::default() } }
6501    pub fn record_frame(&mut self, fps: f32, sim_ms: f32, render_ms: f32) {
6502        if fps < self.min_fps { self.min_fps = fps; }
6503        if fps > self.max_fps { self.max_fps = fps; }
6504        self.avg_fps = (self.avg_fps * self.frame_count as f32 + fps) / (self.frame_count + 1) as f32;
6505        self.avg_sim_ms = (self.avg_sim_ms * self.frame_count as f32 + sim_ms) / (self.frame_count + 1) as f32;
6506        self.avg_render_ms = (self.avg_render_ms * self.frame_count as f32 + render_ms) / (self.frame_count + 1) as f32;
6507        self.frame_count += 1;
6508    }
6509    pub fn total_ms(&self) -> f32 { self.avg_sim_ms + self.avg_render_ms }
6510    pub fn passed_60fps(&self) -> bool { self.avg_fps >= 60.0 }
6511    pub fn grade(&self) -> &'static str {
6512        if self.avg_fps >= 120.0 { "Excellent" } else if self.avg_fps >= 60.0 { "Good" } else if self.avg_fps >= 30.0 { "Acceptable" } else { "Poor" }
6513    }
6514}
6515
6516// ── Particle Simulation Scenarios ────────────────────────────────────────────
6517
6518#[derive(Clone, Debug)]
6519pub struct ParticleScenario {
6520    pub name: String,
6521    pub description: String,
6522    pub emitter_configs: Vec<String>,
6523    pub duration_secs: f32,
6524    pub expected_peak_particles: u32,
6525    pub benchmark: Option<ParticleBenchmarkResult>,
6526}
6527
6528impl ParticleScenario {
6529    pub fn new(name: impl Into<String>, desc: impl Into<String>) -> Self {
6530        Self { name: name.into(), description: desc.into(), emitter_configs: Vec::new(), duration_secs: 10.0, expected_peak_particles: 1000, benchmark: None }
6531    }
6532    pub fn add_emitter_config(&mut self, config: impl Into<String>) { self.emitter_configs.push(config.into()); }
6533    pub fn set_expected_peak(&mut self, n: u32) { self.expected_peak_particles = n; }
6534    pub fn has_benchmark(&self) -> bool { self.benchmark.is_some() }
6535    pub fn benchmark_grade(&self) -> Option<&'static str> { self.benchmark.as_ref().map(|b| b.grade()) }
6536}
6537
6538pub fn build_stress_test_scenarios() -> Vec<ParticleScenario> {
6539    let mut s1 = ParticleScenario::new("Low Load", "Single small emitter");
6540    s1.set_expected_peak(500);
6541    let mut s2 = ParticleScenario::new("Medium Load", "Multiple emitters");
6542    s2.set_expected_peak(10000);
6543    let mut s3 = ParticleScenario::new("High Load", "Many emitters with trails");
6544    s3.set_expected_peak(100000);
6545    let mut s4 = ParticleScenario::new("Extreme Load", "Maximum particle count stress test");
6546    s4.set_expected_peak(500000);
6547    vec![s1, s2, s3, s4]
6548}
6549
6550// ── Final particle constants ──────────────────────────────────────────────────
6551
6552pub const PARTICLE_EDITOR_UNDO_MAX: usize = 100;
6553pub const PARTICLE_EXPORT_FORMATS: &[&str] = &["json", "binary", "xml", "custom"];
6554pub const PARTICLE_BENCH_FRAME_MIN: u64 = 300;
6555pub const PARTICLE_SCENARIO_MAX: usize = 16;
6556pub const PARTICLE_EMITTER_GRID_DEFAULT_SIZE: f32 = 1.0;
6557pub const PARTICLE_VIEWPORT_FAR_PLANE: f32 = 1000.0;
6558pub const PARTICLE_VIEWPORT_NEAR_PLANE: f32 = 0.01;
6559
6560pub fn particle_editor_full_info() -> String {
6561    format!(
6562        "ParticleSystemEditor — {} feature modules, editor + runtime + benchmark pipeline",
6563        particle_module_count()
6564    )
6565}
6566
6567
6568// ── Particle Rotation Over Lifetime ──────────────────────────────────────────
6569
6570#[derive(Clone, Debug)]
6571pub struct RotationOverLifetime {
6572    pub curve: FloatCurveEx,
6573    pub start_rotation: f32,
6574    pub randomize_start: bool,
6575    pub angular_velocity: f32,
6576}
6577
6578impl RotationOverLifetime {
6579    pub fn constant(angular_vel: f32) -> Self {
6580        Self { curve: FloatCurveEx::constant("rot", angular_vel), start_rotation: 0.0, randomize_start: false, angular_velocity: angular_vel }
6581    }
6582    pub fn evaluate_angle(&self, t: f32, lifetime: f32) -> f32 {
6583        self.start_rotation + self.curve.evaluate(t) * lifetime
6584    }
6585    pub fn random_start(mut self) -> Self { self.randomize_start = true; self }
6586}
6587
6588impl Default for RotationOverLifetime {
6589    fn default() -> Self { Self::constant(0.0) }
6590}
6591
6592// ── Particle Gravity Modifier ─────────────────────────────────────────────────
6593
6594#[derive(Clone, Debug)]
6595pub struct GravityModifier {
6596    pub scale: FloatCurveEx,
6597    pub direction: Vec3,
6598}
6599
6600impl GravityModifier {
6601    pub fn new(scale: f32) -> Self { Self { scale: FloatCurveEx::constant("gravity", scale), direction: Vec3::new(0.0, -1.0, 0.0) } }
6602    pub fn no_gravity() -> Self { Self::new(0.0) }
6603    pub fn reverse_gravity() -> Self { Self { scale: FloatCurveEx::constant("gravity", -1.0), direction: Vec3::new(0.0, -1.0, 0.0) } }
6604    pub fn evaluate(&self, t: f32) -> Vec3 { self.direction * self.scale.evaluate(t) * 9.81 }
6605}
6606
6607impl Default for GravityModifier {
6608    fn default() -> Self { Self::new(1.0) }
6609}
6610
6611// ── Particle Emission Shape Sampler ──────────────────────────────────────────
6612
6613#[derive(Clone, Debug)]
6614pub struct ShapeSampler {
6615    pub shape: SpawnerShape,
6616    pub align_to_normal: bool,
6617    pub random_direction_amount: f32,
6618}
6619
6620impl ShapeSampler {
6621    pub fn new(shape: SpawnerShape) -> Self { Self { shape, align_to_normal: false, random_direction_amount: 1.0 } }
6622    pub fn point_sampler() -> Self { Self::new(SpawnerShape::point()) }
6623    pub fn sphere_sampler(radius: f32) -> Self { Self::new(SpawnerShape::sphere(radius)) }
6624    pub fn sample_position(&self, u: f32, v: f32, w: f32) -> Vec3 {
6625        match &self.shape.shape_type {
6626            SpawnerShapeType::Point => Vec3::ZERO,
6627            SpawnerShapeType::Sphere => random_in_sphere(u, v, w) * self.shape.scale.x,
6628            SpawnerShapeType::Circle => Vec3::new((u * 2.0 - 1.0) * self.shape.scale.x, 0.0, (v * 2.0 - 1.0) * self.shape.scale.z),
6629            SpawnerShapeType::Box => Vec3::new((u * 2.0 - 1.0) * self.shape.scale.x, (v * 2.0 - 1.0) * self.shape.scale.y, (w * 2.0 - 1.0) * self.shape.scale.z),
6630            SpawnerShapeType::Cone => {
6631                let angle = self.shape.scale.x.to_radians();
6632                let h = v * self.shape.scale.y;
6633                let r = h * angle.tan();
6634                Vec3::new((u * 2.0 - 1.0) * r, h, (w * 2.0 - 1.0) * r)
6635            }
6636            _ => Vec3::ZERO,
6637        }
6638    }
6639    pub fn sample_direction(&self, pos: Vec3) -> Vec3 {
6640        match self.shape.shape_type {
6641            SpawnerShapeType::Sphere => if pos.length() > 1e-6 { pos.normalize() } else { Vec3::Y },
6642            SpawnerShapeType::Cone => Vec3::new(pos.x, self.shape.scale.y, pos.z).normalize(),
6643            _ => Vec3::Y,
6644        }
6645    }
6646}
6647
6648// ── Particle Render Mode ──────────────────────────────────────────────────────
6649
6650#[derive(Clone, Debug, PartialEq)]
6651pub enum ParticleRenderModeEx {
6652    Billboard,
6653    StretchedBillboard { velocity_scale: f32, length_scale: f32 },
6654    HorizontalBillboard,
6655    VerticalBillboard,
6656    Mesh { mesh_id: u32 },
6657    Trail,
6658}
6659
6660impl ParticleRenderModeEx {
6661    pub fn is_billboard(&self) -> bool {
6662        matches!(self, ParticleRenderModeEx::Billboard | ParticleRenderModeEx::HorizontalBillboard | ParticleRenderModeEx::VerticalBillboard | ParticleRenderModeEx::StretchedBillboard { .. })
6663    }
6664    pub fn is_mesh(&self) -> bool { matches!(self, ParticleRenderModeEx::Mesh { .. }) }
6665    pub fn name(&self) -> &'static str {
6666        match self {
6667            ParticleRenderModeEx::Billboard => "Billboard",
6668            ParticleRenderModeEx::StretchedBillboard { .. } => "Stretched Billboard",
6669            ParticleRenderModeEx::HorizontalBillboard => "Horizontal Billboard",
6670            ParticleRenderModeEx::VerticalBillboard => "Vertical Billboard",
6671            ParticleRenderModeEx::Mesh { .. } => "Mesh",
6672            ParticleRenderModeEx::Trail => "Trail",
6673        }
6674    }
6675}
6676
6677// ── Particle System Profiler ──────────────────────────────────────────────────
6678
6679#[derive(Clone, Debug, Default)]
6680pub struct ParticleProfilerFrame {
6681    pub frame_id: u64,
6682    pub sim_ms: f32,
6683    pub render_ms: f32,
6684    pub cull_ms: f32,
6685    pub sort_ms: f32,
6686    pub total_ms: f32,
6687    pub particle_count: u32,
6688    pub draw_calls: u32,
6689}
6690
6691#[derive(Clone, Debug)]
6692pub struct ParticleProfilerEx {
6693    pub frames: VecDeque<ParticleProfilerFrame>,
6694    pub max_frames: usize,
6695    pub enabled: bool,
6696}
6697
6698impl ParticleProfilerEx {
6699    pub fn new(max_frames: usize) -> Self { Self { frames: VecDeque::new(), max_frames, enabled: true } }
6700    pub fn record(&mut self, frame: ParticleProfilerFrame) {
6701        if !self.enabled { return; }
6702        if self.frames.len() >= self.max_frames { self.frames.pop_front(); }
6703        self.frames.push_back(frame);
6704    }
6705    pub fn avg_total_ms(&self) -> f32 {
6706        if self.frames.is_empty() { return 0.0; }
6707        self.frames.iter().map(|f| f.total_ms).sum::<f32>() / self.frames.len() as f32
6708    }
6709    pub fn avg_fps(&self) -> f32 { let ms = self.avg_total_ms(); if ms < 1e-6 { 9999.0 } else { 1000.0 / ms } }
6710    pub fn peak_particle_count(&self) -> u32 { self.frames.iter().map(|f| f.particle_count).max().unwrap_or(0) }
6711    pub fn clear(&mut self) { self.frames.clear(); }
6712    pub fn frame_count(&self) -> usize { self.frames.len() }
6713}
6714
6715impl Default for ParticleProfilerEx {
6716    fn default() -> Self { Self::new(300) }
6717}
6718
6719// ── More Particle Constants ───────────────────────────────────────────────────
6720
6721pub const PARTICLE_PROFILER_FRAME_BUFFER: usize = 300;
6722pub const PARTICLE_RENDER_MODE_COUNT: usize = 6;
6723pub const PARTICLE_GRAVITY_MODIFIER_DEFAULT: f32 = 1.0;
6724pub const PARTICLE_ROTATION_MAX_DEG_PER_SEC: f32 = 3600.0;
6725pub const PARTICLE_EMISSION_RATE_MAX: f32 = 100_000.0;
6726pub const PARTICLE_LIFETIME_MIN: f32 = 0.01;
6727pub const PARTICLE_LIFETIME_MAX: f32 = 300.0;
6728pub const PARTICLE_SIZE_MIN: f32 = 0.001;
6729pub const PARTICLE_SIZE_MAX: f32 = 100.0;
6730pub const PARTICLE_SPEED_MAX: f32 = 1_000.0;
6731
6732pub fn validate_particle_lifetime(lifetime: f32) -> f32 { lifetime.clamp(PARTICLE_LIFETIME_MIN, PARTICLE_LIFETIME_MAX) }
6733pub fn validate_particle_size(size: f32) -> f32 { size.clamp(PARTICLE_SIZE_MIN, PARTICLE_SIZE_MAX) }
6734pub fn validate_emission_rate(rate: f32) -> f32 { rate.clamp(0.0, PARTICLE_EMISSION_RATE_MAX) }
6735pub fn validate_particle_speed(speed: f32) -> f32 { speed.clamp(-PARTICLE_SPEED_MAX, PARTICLE_SPEED_MAX) }
6736pub fn is_valid_particle_config(lifetime: f32, size: f32, rate: f32) -> bool {
6737    lifetime >= PARTICLE_LIFETIME_MIN && size >= PARTICLE_SIZE_MIN && rate >= 0.0
6738}
6739
6740
6741// ── Particle Spatial Hash ─────────────────────────────────────────────────────
6742
6743#[derive(Clone, Debug)]
6744pub struct ParticleSpatialHash {
6745    pub cell_size: f32,
6746    pub cells: HashMap<(i32, i32, i32), Vec<u32>>,
6747    pub particle_count: u32,
6748}
6749
6750impl ParticleSpatialHash {
6751    pub fn new(cell_size: f32) -> Self { Self { cell_size, cells: HashMap::new(), particle_count: 0 } }
6752    fn cell_key(&self, pos: Vec3) -> (i32, i32, i32) {
6753        ((pos.x / self.cell_size).floor() as i32, (pos.y / self.cell_size).floor() as i32, (pos.z / self.cell_size).floor() as i32)
6754    }
6755    pub fn insert(&mut self, id: u32, pos: Vec3) {
6756        let key = self.cell_key(pos);
6757        self.cells.entry(key).or_default().push(id);
6758        self.particle_count += 1;
6759    }
6760    pub fn query_radius(&self, center: Vec3, radius: f32) -> Vec<u32> {
6761        let r = (radius / self.cell_size).ceil() as i32 + 1;
6762        let cx = (center.x / self.cell_size).floor() as i32;
6763        let cy = (center.y / self.cell_size).floor() as i32;
6764        let cz = (center.z / self.cell_size).floor() as i32;
6765        let mut result = Vec::new();
6766        for dx in -r..=r { for dy in -r..=r { for dz in -r..=r {
6767            if let Some(ids) = self.cells.get(&(cx+dx, cy+dy, cz+dz)) { result.extend_from_slice(ids); }
6768        }}}
6769        result
6770    }
6771    pub fn clear(&mut self) { self.cells.clear(); self.particle_count = 0; }
6772    pub fn cell_count(&self) -> usize { self.cells.len() }
6773}
6774
6775// ── Particle Boundary Volume ──────────────────────────────────────────────────
6776
6777#[derive(Clone, Debug)]
6778pub struct ParticleAabb {
6779    pub min: Vec3,
6780    pub max: Vec3,
6781}
6782
6783impl ParticleAabb {
6784    pub fn new(min: Vec3, max: Vec3) -> Self { Self { min, max } }
6785    pub fn empty() -> Self { Self { min: Vec3::splat(f32::MAX), max: Vec3::splat(f32::MIN) } }
6786    pub fn extend(&mut self, p: Vec3) {
6787        self.min = Vec3::new(self.min.x.min(p.x), self.min.y.min(p.y), self.min.z.min(p.z));
6788        self.max = Vec3::new(self.max.x.max(p.x), self.max.y.max(p.y), self.max.z.max(p.z));
6789    }
6790    pub fn center(&self) -> Vec3 { (self.min + self.max) * 0.5 }
6791    pub fn size(&self) -> Vec3 { self.max - self.min }
6792    pub fn volume(&self) -> f32 { let s = self.size(); s.x * s.y * s.z }
6793    pub fn contains(&self, p: Vec3) -> bool { p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y && p.z >= self.min.z && p.z <= self.max.z }
6794    pub fn intersects(&self, other: &Self) -> bool { self.min.x <= other.max.x && self.max.x >= other.min.x && self.min.y <= other.max.y && self.max.y >= other.min.y && self.min.z <= other.max.z && self.max.z >= other.min.z }
6795    pub fn is_valid(&self) -> bool { self.min.x <= self.max.x && self.min.y <= self.max.y && self.min.z <= self.max.z }
6796    pub fn expand(&self, amount: f32) -> Self { Self { min: self.min - Vec3::splat(amount), max: self.max + Vec3::splat(amount) } }
6797}
6798
6799// ── Particle Camera Culling ───────────────────────────────────────────────────
6800
6801#[derive(Clone, Debug)]
6802pub struct ParticleFrustumCuller {
6803    pub planes: Vec<Vec4>,
6804    pub cull_count_last_frame: u32,
6805    pub pass_count_last_frame: u32,
6806}
6807
6808impl ParticleFrustumCuller {
6809    pub fn new() -> Self { Self { planes: vec![Vec4::ZERO; 6], cull_count_last_frame: 0, pass_count_last_frame: 0 } }
6810    pub fn set_frustum(&mut self, planes: Vec<Vec4>) { self.planes = planes; }
6811    pub fn test_aabb(&self, aabb: &ParticleAabb) -> bool {
6812        for plane in &self.planes {
6813            let px = if plane.x > 0.0 { aabb.max.x } else { aabb.min.x };
6814            let py = if plane.y > 0.0 { aabb.max.y } else { aabb.min.y };
6815            let pz = if plane.z > 0.0 { aabb.max.z } else { aabb.min.z };
6816            if plane.x * px + plane.y * py + plane.z * pz + plane.w < 0.0 { return false; }
6817        }
6818        true
6819    }
6820    pub fn begin_frame(&mut self) { self.cull_count_last_frame = 0; self.pass_count_last_frame = 0; }
6821    pub fn record_cull(&mut self) { self.cull_count_last_frame += 1; }
6822    pub fn record_pass(&mut self) { self.pass_count_last_frame += 1; }
6823    pub fn cull_rate(&self) -> f32 {
6824        let total = self.cull_count_last_frame + self.pass_count_last_frame;
6825        if total == 0 { 0.0 } else { self.cull_count_last_frame as f32 / total as f32 }
6826    }
6827}
6828
6829impl Default for ParticleFrustumCuller {
6830    fn default() -> Self { Self::new() }
6831}
6832
6833pub const PARTICLE_SPATIAL_HASH_DEFAULT_CELL: f32 = 2.0;
6834pub const PARTICLE_AABB_MARGIN: f32 = 0.1;
6835pub const PARTICLE_CULL_BACKFACE: bool = false;
6836pub const PARTICLE_SORT_BACK_TO_FRONT: bool = true;
6837pub const PARTICLE_GPU_INSTANCING_MAX: u32 = 65536;
6838
6839pub fn particle_system_capabilities() -> HashMap<&'static str, bool> {
6840    let mut caps = HashMap::new();
6841    caps.insert("gpu_simulation", false);
6842    caps.insert("compute_shaders", false);
6843    caps.insert("instanced_rendering", true);
6844    caps.insert("trail_rendering", true);
6845    caps.insert("texture_animation", true);
6846    caps.insert("lod", true);
6847    caps.insert("frustum_culling", true);
6848    caps.insert("spatial_hashing", true);
6849    caps
6850}
6851
6852
6853// ── Particle Warm-Up & Pre-Simulation ────────────────────────────────────────
6854
6855#[derive(Clone, Debug)]
6856pub struct ParticleWarmUp {
6857    pub duration_secs: f32,
6858    pub dt: f32,
6859    pub enabled: bool,
6860}
6861
6862impl ParticleWarmUp {
6863    pub fn new(duration: f32, dt: f32) -> Self { Self { duration_secs: duration, dt: dt.max(0.001), enabled: true } }
6864    pub fn steps(&self) -> u32 { (self.duration_secs / self.dt) as u32 }
6865    pub fn disable(mut self) -> Self { self.enabled = false; self }
6866    pub fn one_second() -> Self { Self::new(1.0, 0.033) }
6867}
6868
6869impl Default for ParticleWarmUp {
6870    fn default() -> Self { Self::new(0.0, 0.033) }
6871}
6872
6873// ── Particle Data Buffer ──────────────────────────────────────────────────────
6874
6875#[derive(Clone, Debug)]
6876pub struct ParticleDataBuffer {
6877    pub positions: Vec<Vec3>,
6878    pub velocities: Vec<Vec3>,
6879    pub colors: Vec<Vec4>,
6880    pub sizes: Vec<f32>,
6881    pub lifetimes: Vec<f32>,
6882    pub ages: Vec<f32>,
6883    pub rotations: Vec<f32>,
6884    pub capacity: u32,
6885    pub active_count: u32,
6886}
6887
6888impl ParticleDataBuffer {
6889    pub fn new(capacity: u32) -> Self {
6890        let n = capacity as usize;
6891        Self { positions: vec![Vec3::ZERO; n], velocities: vec![Vec3::ZERO; n], colors: vec![Vec4::ONE; n], sizes: vec![1.0; n], lifetimes: vec![1.0; n], ages: vec![0.0; n], rotations: vec![0.0; n], capacity, active_count: 0 }
6892    }
6893    pub fn is_alive(&self, idx: usize) -> bool { idx < self.active_count as usize && self.ages[idx] < self.lifetimes[idx] }
6894    pub fn age_normalized(&self, idx: usize) -> f32 {
6895        let lt = self.lifetimes[idx];
6896        if lt <= 0.0 { 1.0 } else { (self.ages[idx] / lt).clamp(0.0, 1.0) }
6897    }
6898    pub fn active_count(&self) -> u32 { self.active_count }
6899    pub fn capacity(&self) -> u32 { self.capacity }
6900    pub fn utilization(&self) -> f32 { if self.capacity == 0 { 0.0 } else { self.active_count as f32 / self.capacity as f32 } }
6901    pub fn clear(&mut self) { self.active_count = 0; }
6902    pub fn tick_ages(&mut self, dt: f32) { for i in 0..self.active_count as usize { self.ages[i] += dt; } }
6903}
6904
6905// ── Final constants ───────────────────────────────────────────────────────────
6906
6907pub const PARTICLE_WARMUP_MAX_SECS: f32 = 30.0;
6908pub const PARTICLE_DATA_BUFFER_DEFAULT: u32 = 65536;
6909pub const PARTICLE_MAX_ACTIVE_SYSTEMS: u32 = 128;
6910pub const PARTICLE_TICK_RATE_DEFAULT: f32 = 60.0;
6911
6912pub fn particle_data_buffer_size_bytes(capacity: u32) -> u64 {
6913    let n = capacity as u64;
6914    n * (12 + 12 + 16 + 4 + 4 + 4 + 4)  // vec3 + vec3 + vec4 + f32*4
6915}
6916
6917pub fn describe_blend_mode(mode: &BlendMode) -> &'static str {
6918    match mode {
6919        BlendMode::Additive => "Adds particle color to background — bright, glowing look",
6920        BlendMode::Alpha => "Standard transparency — alpha channel used",
6921        BlendMode::Multiply => "Darkens background — shadows, stains",
6922        BlendMode::Screen => "Lightens background — soft glows",
6923        BlendMode::Premultiplied => "Alpha pre-multiplied — avoids fringing",
6924    }
6925}
6926
6927
6928// ── Particle Effect Summary ───────────────────────────────────────────────────
6929
6930pub struct ParticleEffectSummary {
6931    pub name: String,
6932    pub emitter_count: u32,
6933    pub max_particles: u32,
6934    pub duration: f32,
6935    pub looping: bool,
6936    pub has_trails: bool,
6937    pub has_sub_emitters: bool,
6938    pub texture_count: u32,
6939    pub blend_mode: String,
6940    pub lod_levels: u32,
6941    pub estimated_cost: f32,
6942}
6943
6944impl ParticleEffectSummary {
6945    pub fn estimate_cost(particles: u32, has_trails: bool, has_sub_emitters: bool) -> f32 {
6946        let base = particles as f32 * 0.001;
6947        let trail_mult = if has_trails { 2.0 } else { 1.0 };
6948        let sub_mult = if has_sub_emitters { 1.5 } else { 1.0 };
6949        base * trail_mult * sub_mult
6950    }
6951    pub fn is_expensive(&self) -> bool { self.estimated_cost > 10.0 }
6952    pub fn is_cheap(&self) -> bool { self.estimated_cost < 1.0 }
6953    pub fn performance_grade(&self) -> &'static str {
6954        if self.is_cheap() { "Light" } else if self.is_expensive() { "Heavy" } else { "Medium" }
6955    }
6956}
6957
6958pub fn particle_system_ready() -> bool { true }
6959pub fn particle_version() -> &'static str { "ParticleSystemEditor v2.0" }
6960pub fn max_safe_particles_for_target_fps(target_fps: f32, ms_budget: f32) -> u32 { ((ms_budget / (1000.0 / target_fps)) * 50000.0) as u32 }
6961pub fn particles_60fps_budget() -> u32 { max_safe_particles_for_target_fps(60.0, 3.0) }
6962pub fn describe_attractor_type(t: &AttractorType) -> &'static str {
6963    match t {
6964        AttractorType::Attract => "Pulls particles toward center",
6965        AttractorType::Repel => "Pushes particles away from center",
6966        AttractorType::Vortex => "Spins particles in a spiral",
6967        AttractorType::Drag => "Slows particle movement",
6968        AttractorType::Gravity => "Directional gravity pull",
6969        AttractorType::Wind => "Steady wind force",
6970    }
6971}
6972
6973
6974pub const PARTICLE_EFFECT_COST_LIGHT_THRESHOLD: f32 = 1.0;
6975pub const PARTICLE_EFFECT_COST_MEDIUM_THRESHOLD: f32 = 10.0;
6976pub const PARTICLE_EFFECT_COST_HEAVY_THRESHOLD: f32 = 50.0;
6977pub const PARTICLE_MS_BUDGET_60FPS: f32 = 16.666;
6978pub const PARTICLE_MS_BUDGET_30FPS: f32 = 33.333;
6979pub const PARTICLE_SIMULATION_SAFE_BUDGET_MS: f32 = 3.0;
6980pub const PARTICLE_RENDER_SAFE_BUDGET_MS: f32 = 2.0;
6981pub fn particle_budget_ok(sim_ms: f32, render_ms: f32) -> bool {
6982    sim_ms <= PARTICLE_SIMULATION_SAFE_BUDGET_MS && render_ms <= PARTICLE_RENDER_SAFE_BUDGET_MS
6983}
6984pub fn particle_quality_from_budget(ms_available: f32) -> &'static str {
6985    if ms_available >= 8.0 { "Ultra" } else if ms_available >= 4.0 { "High" } else if ms_available >= 2.0 { "Medium" } else { "Low" }
6986}
6987pub fn particle_count_for_quality(quality: &str) -> u32 {
6988    match quality { "Ultra" => 500_000, "High" => 100_000, "Medium" => 25_000, _ => 5_000 }
6989}
6990
6991
6992pub fn emitter_type_name(is_burst: bool, is_looping: bool) -> &'static str {
6993    match (is_burst, is_looping) {
6994        (true, _) => "Burst",
6995        (false, true) => "Continuous Looping",
6996        (false, false) => "Continuous One-Shot",
6997    }
6998}
6999pub fn particle_system_build_info() -> String {
7000    format!("Build: ParticleSystemEditor, modules={}, max_particles={}", particle_module_count(), PARTICLE_MAX_TOTAL)
7001}
7002