Skip to main content

primase_core/
clip_effects.rs

1//! `core.clip_effects`: the particle systems a clip starts while it plays, and where and when. A clip's events say that
2//! something happens at a time ([`ClipEvents`](crate::ClipEvents)); a source's lowering that knows an event starts an effect
3//! says it here, in terms a player can follow without knowing the source: which system, from when, standing where.
4
5use crate::particles::ParticleSystem;
6use crate::world::Linearity;
7use crate::{Clip, Component, Entity, Geometry, JsonError, Linear, Transform, World};
8use serde::Serialize;
9use serde_json::Value;
10
11/// `core.clip_effects`, on a clip.
12#[derive(Clone, Debug, Default, PartialEq, Serialize)]
13pub struct ClipEffects {
14    /// Where the record was made from (a provenance string), if the lowering states it.
15    pub origin: Option<String>,
16    /// The systems the clip starts, in source order.
17    pub spawns: Vec<ClipSpawn>,
18    /// What the lowering could not carry, by reason, with how many: accounting, never state.
19    pub lost: Vec<(String, u32)>,
20}
21
22/// One system a clip starts.
23#[derive(Clone, Debug, PartialEq, Serialize)]
24pub struct ClipSpawn {
25    /// The system: an entity holding a [`ParticleSystem`] and no [`Transform`] (it is placed by this record).
26    pub system: Entity,
27    /// The clip time it starts at, seconds.
28    pub start: f32,
29    /// The clip time it is stopped at, if the source stops it; `None` lets it play out.
30    pub end: Option<f32>,
31    /// The name of the socket of the clip's rig the system stands at (a socket's name is the source's: provenance, but the one
32    /// handle a clip has on a rig's sockets); `None` stands it at the rig's origin. A name the rig's skeleton does not hold
33    /// is looked for on the rigs attached to it (`core.attachment`: what the rig holds, a weapon in a hand), the attached
34    /// rig standing where its attachment places it and in its own pose.
35    pub socket: Option<String>,
36    /// The further sockets the system runs through after `socket`, in order (a weapon's trail through its blade's sockets
37    /// from the hilt to the tip), looked up as `socket` is. The system stands at each of its sockets at once: a trail it
38    /// draws is the band its sockets sweep, the path of each joined to the path of the next, and the trail's own width
39    /// is not used. Empty stands it at `socket` alone.
40    pub span: Vec<String>,
41    /// Whether the system follows its socket as the clip moves it, else it stays where it started.
42    pub follow: bool,
43    /// The index in the clip's `core.clip_events` of the event that starts it, where the source says.
44    pub event: Option<u32>,
45}
46
47impl Component for ClipEffects {
48    const NAME: &'static str = "core.clip_effects";
49    const GEOMETRY: Geometry = Geometry::None;
50    fn to_json(&self) -> Result<Option<Value>, JsonError> {
51        crate::json::to_value(self).map(Some)
52    }
53    fn references(&self, out: &mut Vec<Entity>) {
54        out.extend(self.spawns.iter().map(|spawn| spawn.system));
55    }
56    /// On a clip; each spawn names a system that is not placed, a finite start that is not negative, an end that is not before
57    /// it and a socket name that is not empty.
58    fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
59        if !world.has::<Clip>(entity) {
60            return Err("core.clip_effects must be on a core.clip".into());
61        }
62        if self
63            .origin
64            .as_ref()
65            .is_some_and(|origin| origin.trim().is_empty())
66        {
67            return Err("clip effects origin is empty".into());
68        }
69        if self.lost.iter().any(|(reason, _)| reason.trim().is_empty()) {
70            return Err("a lost count without a reason".into());
71        }
72        for (i, spawn) in self.spawns.iter().enumerate() {
73            if !world.has::<ParticleSystem>(spawn.system) {
74                return Err(format!("spawn {i}: {:?} is not a particle system", spawn.system));
75            }
76            if world.has::<Transform>(spawn.system) {
77                return Err(format!(
78                    "spawn {i}: the system {:?} is placed; a clip places the systems it starts",
79                    spawn.system
80                ));
81            }
82            if !spawn.start.is_finite() || spawn.start < 0.0 {
83                return Err(format!("spawn {i}: a start of {} seconds", spawn.start));
84            }
85            if spawn.end.is_some_and(|end| !end.is_finite() || end < spawn.start) {
86                return Err(format!("spawn {i}: an end before its start or not finite"));
87            }
88            if spawn
89                .socket
90                .as_ref()
91                .is_some_and(|socket| socket.trim().is_empty())
92            {
93                return Err(format!("spawn {i}: a socket name that is empty"));
94            }
95            if !spawn.span.is_empty() && spawn.socket.is_none() {
96                return Err(format!("spawn {i}: sockets it runs through with no first"));
97            }
98            if spawn.span.iter().any(|s| s.trim().is_empty()) {
99                return Err(format!("spawn {i}: a socket it runs through whose name is empty"));
100            }
101            let mut through = std::collections::BTreeSet::new();
102            if let Some(again) = spawn
103                .socket
104                .iter()
105                .chain(&spawn.span)
106                .find(|s| !through.insert(*s))
107            {
108                return Err(format!("spawn {i}: it runs through the socket {again} twice"));
109            }
110        }
111        Ok(())
112    }
113    fn apply_linear(&self, _linear: &Linear) -> Linearity<Self> {
114        Linearity::Invariant
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::Extrapolation;
122    use crate::particles::Termination;
123
124    fn system() -> ParticleSystem {
125        ParticleSystem {
126            seed: 0,
127            emitters: Vec::new(),
128            inputs: Vec::new(),
129            states: 1,
130            warmup: 0.0,
131            time_scale: None,
132            max_view_distance: None,
133            termination: Termination::Natural,
134            end: None,
135            lost: Vec::new(),
136        }
137    }
138
139    fn fixture() -> (World, Entity, Entity, ClipEffects) {
140        let mut world = World::new();
141        let clip = world.spawn();
142        world.insert(
143            clip,
144            Clip {
145                name: "clip".into(),
146                duration: 1.0,
147                extrapolation: Extrapolation::Hold,
148                channels: vec![],
149                root_motion: None,
150                additive: None,
151            },
152        );
153        let spark = world.spawn();
154        world.insert(spark, system());
155        let effects = ClipEffects {
156            origin: Some("source event record".into()),
157            spawns: vec![ClipSpawn {
158                system: spark,
159                start: 0.25,
160                end: Some(0.5),
161                socket: Some("dummy 100".into()),
162                span: Vec::new(),
163                follow: true,
164                event: Some(3),
165            }],
166            lost: vec![("an effect named through a param row".into(), 2)],
167        };
168        (world, clip, spark, effects)
169    }
170
171    #[test]
172    fn a_spawn_of_an_unplaced_system_on_a_clip_verifies_and_names_its_system() {
173        let (world, clip, spark, effects) = fixture();
174        assert_eq!(effects.verify(clip, &world), Ok(()));
175        let mut referenced = Vec::new();
176        effects.references(&mut referenced);
177        assert_eq!(referenced, [spark]);
178        let json = effects.to_json().unwrap().unwrap();
179        assert_eq!(json["spawns"][0]["socket"], "dummy 100");
180        assert_eq!(json["lost"][0][1], 2);
181    }
182
183    #[test]
184    fn what_is_not_a_clip_a_system_or_a_time_is_refused() {
185        let (mut world, clip, spark, effects) = fixture();
186        // not on a clip
187        assert!(effects.verify(spark, &world).is_err());
188        // a spawn of something that is not a system, and of a system that is placed
189        let mut not_a_system = effects.clone();
190        not_a_system.spawns[0].system = clip;
191        assert!(not_a_system.verify(clip, &world).is_err());
192        world.insert(spark, Transform::IDENTITY);
193        assert!(effects.verify(clip, &world).is_err());
194        world.remove::<Transform>(spark);
195        for (start, end) in [
196            (f32::NAN, None),
197            (-0.5, None),
198            (0.5, Some(0.25)),
199            (0.0, Some(f32::INFINITY)),
200        ] {
201            let mut bad = effects.clone();
202            bad.spawns[0].start = start;
203            bad.spawns[0].end = end;
204            assert!(bad.verify(clip, &world).is_err(), "{start} {end:?}");
205        }
206        let mut bad = effects.clone();
207        bad.spawns[0].socket = Some(" ".into());
208        assert!(bad.verify(clip, &world).is_err());
209        // sockets it runs through: with a first, named, each once
210        let mut bad = effects.clone();
211        bad.spawns[0].span = vec!["dummy 101".into(), "dummy 102".into()];
212        assert_eq!(bad.verify(clip, &world), Ok(()));
213        bad.spawns[0].socket = None;
214        assert!(bad.verify(clip, &world).is_err());
215        bad.spawns[0].socket = Some("dummy 100".into());
216        bad.spawns[0].span = vec![String::new()];
217        assert!(bad.verify(clip, &world).is_err());
218        bad.spawns[0].span = vec!["dummy 101".into(), "dummy 101".into()];
219        assert!(bad.verify(clip, &world).is_err());
220        bad.spawns[0].span = vec!["dummy 100".into()];
221        assert!(bad.verify(clip, &world).is_err());
222        let mut bad = effects;
223        bad.lost.push((String::new(), 1));
224        assert!(bad.verify(clip, &world).is_err());
225    }
226}