Skip to main content

primase_core/
clip_events.rs

1//! Source-ordered clip events for display and typed consumers. Reading an event does not execute it.
2
3use crate::world::Linearity;
4use crate::{Clip, Component, Entity, Geometry, JsonError, Linear, NameOrigin, World};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// `core.clip_events`, on a clip. Refusals name source records a lowering could not
9/// interpret; the lowering must not emit a partial behavior timeline for those records.
10#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct ClipEvents {
13    pub origin: Option<String>,
14    pub events: Vec<ClipEvent>,
15    pub refusals: Vec<String>,
16}
17
18#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct ClipEvent {
21    pub kind: i64,
22    /// A name and its naming origin travel together, or both are absent.
23    pub name: Option<String>,
24    pub naming: Option<NameOrigin>,
25    /// Seconds from the clip's start, finite. It may be below 0: source events legitimately begin before their clip
26    /// does, and core neither clamps nor discards them; what that means for playback is each consumer's to read.
27    pub start: f32,
28    /// None runs to the clip's end. A finite explicit end may exceed clip duration;
29    /// source events legitimately do so and core neither clamps nor discards them.
30    pub end: Option<f32>,
31    /// Source order, including duplicate names: consumers address fields by index.
32    pub values: Vec<EventValue>,
33    /// Stored bytes stay available even when typed fields were decoded.
34    pub bytes: Vec<u8>,
35}
36
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct EventValue {
40    /// May repeat within an event; labels do not replace source-field identity.
41    pub name: String,
42    pub value: EventValueData,
43}
44
45#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46pub enum EventValueData {
47    I64(i64),
48    F64(f64),
49    Bool(bool),
50    Text(String),
51    Bytes(Vec<u8>),
52}
53
54impl Component for ClipEvents {
55    const NAME: &'static str = "core.clip_events";
56    const GEOMETRY: Geometry = Geometry::None;
57    fn to_json(&self) -> Result<Option<Value>, JsonError> {
58        crate::json::to_value(self).map(Some)
59    }
60    fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
61        if !world.has::<Clip>(entity) {
62            return Err("core.clip_events must be on a core.clip".into());
63        }
64        if self.origin.as_ref().is_some_and(|s| s.trim().is_empty()) {
65            return Err("clip event origin is empty".into());
66        }
67        if self.refusals.iter().any(|s| s.trim().is_empty()) {
68            return Err("clip event refusal is empty".into());
69        }
70        for (i, event) in self.events.iter().enumerate() {
71            if !event.start.is_finite() || event.end.is_some_and(|end| !end.is_finite() || end < event.start)
72            {
73                return Err(format!(
74                    "event {i}: start must be finite, end finite and at least start"
75                ));
76            }
77            if event.name.is_some() != event.naming.is_some()
78                || event.name.as_ref().is_some_and(|s| s.trim().is_empty())
79            {
80                return Err(format!(
81                    "event {i}: name and naming origin must be present together and nonempty"
82                ));
83            }
84            if let Some(naming) = &event.naming {
85                naming.verify().map_err(|reason| format!("event {i}: {reason}"))?;
86            }
87            for (at, field) in event.values.iter().enumerate() {
88                if field.name.trim().is_empty() {
89                    return Err(format!("event {i} field {at}: name is empty"));
90                }
91                if matches!(field.value,EventValueData::F64(v) if !v.is_finite()) {
92                    return Err(format!("event {i} field {at}: numeric value is not finite"));
93                }
94            }
95        }
96        Ok(())
97    }
98    fn apply_linear(&self, _linear: &Linear) -> Linearity<Self> {
99        Linearity::Invariant
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::Extrapolation;
107
108    fn fixture() -> (World, Entity, ClipEvents) {
109        let mut world = World::new();
110        let entity = world.spawn();
111        world.insert(
112            entity,
113            Clip {
114                name: "clip".into(),
115                duration: 1.0,
116                extrapolation: Extrapolation::Hold,
117                channels: vec![],
118                root_motion: None,
119                additive: None,
120            },
121        );
122        let events = ClipEvents {
123            origin: Some("source event record".into()),
124            refusals: vec!["unknown behavior kept raw".into()],
125            events: vec![ClipEvent {
126                kind: 193,
127                name: Some("Opacity".into()),
128                naming: Some(NameOrigin::Community {
129                    credit: "template author".into(),
130                }),
131                start: 0.25,
132                end: Some(2.0),
133                bytes: vec![0, 1, 255],
134                values: vec![
135                    EventValue {
136                        name: "value".into(),
137                        value: EventValueData::I64(-2),
138                    },
139                    EventValue {
140                        name: "value".into(),
141                        value: EventValueData::F64(0.5),
142                    },
143                    EventValue {
144                        name: "enabled".into(),
145                        value: EventValueData::Bool(true),
146                    },
147                    EventValue {
148                        name: "label".into(),
149                        value: EventValueData::Text("source".into()),
150                    },
151                    EventValue {
152                        name: "raw".into(),
153                        value: EventValueData::Bytes(vec![255, 0]),
154                    },
155                ],
156            }],
157        };
158        (world, entity, events)
159    }
160
161    #[test]
162    fn all_typed_values_and_duplicate_labels_roundtrip_in_source_order() {
163        let (mut world, entity, events) = fixture();
164        assert!(
165            events.verify(entity, &world).is_ok(),
166            "explicit end may outlast the clip"
167        );
168        let value = events.to_json().unwrap().unwrap();
169        let copy: ClipEvents = serde_json::from_value(value).unwrap();
170        assert_eq!(copy, events);
171        assert_eq!(copy.events[0].values[0].name, copy.events[0].values[1].name);
172        world.insert(entity, events.clone());
173        let subset = world.subset(&[entity].into());
174        assert_eq!(subset.get::<ClipEvents>(entity), Some(&events));
175        assert!(
176            events.remap_primitives(&[2, 0, 2]).is_none(),
177            "events are not mesh-primitive data"
178        );
179        assert!(matches!(
180            events.apply_linear(&Linear::mirror_x()),
181            Linearity::Invariant
182        ));
183    }
184
185    #[test]
186    fn a_start_before_the_clip_is_kept_as_stored() {
187        let (world, entity, events) = fixture();
188        for (start, end) in [(-1.0 / 30.0, Some(0.3)), (-0.5, None), (-0.25, Some(-0.25))] {
189            let mut e = events.clone();
190            e.events[0].start = start;
191            e.events[0].end = end;
192            assert!(e.verify(entity, &world).is_ok(), "{start}..{end:?}");
193            let copy: ClipEvents = serde_json::from_value(e.to_json().unwrap().unwrap()).unwrap();
194            assert_eq!(copy.events[0].start, start);
195        }
196    }
197
198    #[test]
199    fn malformed_times_names_and_nonfinite_fields_refuse() {
200        let (world, entity, events) = fixture();
201        for (start, end) in [
202            (f32::NAN, None),
203            (f32::NEG_INFINITY, Some(0.25)),
204            (0.5, Some(0.25)),
205            (-0.1, Some(-0.2)),
206            (0.0, Some(f32::INFINITY)),
207        ] {
208            let mut e = events.clone();
209            e.events[0].start = start;
210            e.events[0].end = end;
211            assert!(e.verify(entity, &world).is_err());
212        }
213        let mut e = events.clone();
214        e.events[0].naming = None;
215        assert!(e.verify(entity, &world).is_err());
216        let mut e = events.clone();
217        e.events[0].name = None;
218        assert!(e.verify(entity, &world).is_err());
219        let mut e = events.clone();
220        e.events[0].naming = Some(NameOrigin::Community { credit: " ".into() });
221        assert!(e.verify(entity, &world).is_err());
222        let mut e = events.clone();
223        e.refusals.push(String::new());
224        assert!(e.verify(entity, &world).is_err());
225        let mut e = events.clone();
226        e.origin = Some(String::new());
227        assert!(e.verify(entity, &world).is_err());
228        let mut e = events.clone();
229        e.events[0].values[0].value = EventValueData::F64(f64::NAN);
230        assert!(e.verify(entity, &world).is_err());
231        assert!(e.to_json().is_err());
232        let mut e = events.clone();
233        e.events[0].end = None;
234        assert!(e.verify(entity, &world).is_ok());
235        assert!(e.verify(Entity::from_index(99), &world).is_err());
236        let mut value = events.to_json().unwrap().unwrap();
237        value["unknown"] = serde_json::json!(true);
238        assert!(serde_json::from_value::<ClipEvents>(value).is_err());
239    }
240}