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#[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#[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#[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#[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#[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#[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, pub charge: f32, pub enabled: bool,
755}
756impl MagneticForce {
757 pub fn new(b: Vec3, charge: f32) -> Self { Self { field_vector: b, charge, enabled: true } }
758 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
847static 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#[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#[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#[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#[derive(Clone, Debug)]
1122pub struct Particle {
1123 pub position: Vec3,
1124 pub prev_position: Vec3, 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 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#[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#[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 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 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 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 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 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 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 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 if self.gravity_module.enabled {
1430 p.apply_force(gravity);
1431 }
1432
1433 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 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 p.integrate_verlet(dt);
1448
1449 if col_mod.enabled {
1451 col_mod.resolve(&mut p.position, &mut p.velocity, p.size);
1452 }
1453
1454 p.update_rotation(dt);
1456
1457 if color_mod.enabled {
1459 p.color = color_mod.color_over_lifetime.evaluate(t);
1460 }
1461
1462 if size_mod.enabled {
1464 p.size *= size_mod.size_over_lifetime.evaluate(t);
1465 }
1466
1467 if tex_mod.enabled {
1469 p.frame = tex_mod.get_frame(p.age, p.lifetime);
1470 }
1471 }
1472
1473 self.particles.retain(|p| p.alive);
1475
1476 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 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#[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
1552pub 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
1890impl EmitterShape {
1892 fn _cylinder_check() {}
1893}
1894
1895#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
3002pub 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
3058pub 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#[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#[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#[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#[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
3767impl EmitterShape {
3772 pub fn cylinder(radius: f32, height: f32) -> Self {
3773 EmitterShape::Cone { radius, angle_deg: 0.0, length: height }
3774 }
3775}
3776
3777pub 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); p.integrate_verlet(0.016);
3815 p.position.x > 0.0
3816}
3817
3818pub fn test_lorentz() -> bool {
3819 let mf = MagneticForce::new(Vec3::Y, 1.0);
3821 let f = mf.apply(Vec3::X);
3822 (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 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 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
3958pub 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 }
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
4044pub 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
4091pub 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#[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#[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
4200pub 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#[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
4259pub 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#[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
4327pub 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
4360pub 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
4391pub 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
4421pub 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
4443pub 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
4556impl 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
4582pub 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
4600pub 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
5378pub fn simplex_noise_3d_approx(p: Vec3) -> f32 {
5383 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#[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#[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#[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#[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#[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#[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#[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#[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#[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
5814pub 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#[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#[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#[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#[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#[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#[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
6101pub 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#[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 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#[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#[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#[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#[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#[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#[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
6354pub 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#[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#[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#[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#[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
6550pub 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#[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#[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#[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#[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#[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
6719pub 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#[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#[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#[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#[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#[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
6905pub 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) }
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
6928pub 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