motion_content/particles/events.rs
1//! Stable event policies and sub-emitter inheritance, independent of rendering.
2
3use super::*;
4
5/// Fixed-boundary particle event.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub enum ParticleEventKind {
8 /// First admission of a live particle.
9 Birth,
10 /// Half-open lifetime expiry, including kill-on-hit.
11 Death,
12 /// Geometric or population contact.
13 Collision,
14 /// First boundary at/after an absolute age in seconds.
15 AgeReached(f32),
16}
17
18/// Event payload ordered by stable nonrecycled particle ID.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct ParticleEvent {
21 /// Exact simulation boundary time (including prewarm offset).
22 pub time: Time,
23 /// Particle state at the event.
24 pub particle: Particle,
25 /// Trigger kind.
26 pub kind: ParticleEventKind,
27}
28
29/// Parent properties added to an emitter's local child record.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum Inherit {
32 /// Keep emitter-authored values.
33 None,
34 /// Translate local emission to the event location.
35 #[default]
36 Position,
37 /// Translate and add parent velocity.
38 Velocity,
39 /// Translate and multiply parent premultiplied color.
40 Color,
41 /// Translate, add velocity and inherit color.
42 All,
43}
44
45/// Event-triggered emitter. It receives local step zero and Time::ZERO for each trigger.
46#[derive(Clone)]
47pub struct SubEmitter {
48 /// Birth/death/contact/age trigger.
49 pub on: ParticleEventKind,
50 /// Immutable local emitter definition.
51 pub emitter: Arc<dyn ParticleEmitter>,
52 /// Parent property policy.
53 pub inherit: Inherit,
54 /// Probability in 0..=1, keyed by parent/event/rule.
55 pub probability: f32,
56}
57
58impl SubEmitter {
59 pub(super) fn validate(&self) -> Result<(), ValidationError> {
60 if !self.probability.is_finite() || !(0.0..=1.0).contains(&self.probability) {
61 return Err(invalid(
62 "sub_emitter",
63 "Probability must be finite in 0..=1",
64 ));
65 }
66 if let ParticleEventKind::AgeReached(age) = self.on
67 && (!age.is_finite() || age <= 0.0 || age > 3600.0)
68 {
69 return Err(invalid(
70 "sub_emitter",
71 "Age threshold must be finite in (0,3600]",
72 ));
73 }
74 Ok(())
75 }
76}