1use crate::world::Linearity;
4use crate::{Clip, Component, Entity, Geometry, JsonError, Linear, NameOrigin, World};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[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 pub name: Option<String>,
24 pub naming: Option<NameOrigin>,
25 pub start: f32,
28 pub end: Option<f32>,
31 pub values: Vec<EventValue>,
33 pub bytes: Vec<u8>,
35}
36
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct EventValue {
40 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}