use crate::world::Linearity;
use crate::{Clip, Component, Entity, Geometry, JsonError, Linear, NameOrigin, World};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClipEvents {
pub origin: Option<String>,
pub events: Vec<ClipEvent>,
pub refusals: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClipEvent {
pub kind: i64,
pub name: Option<String>,
pub naming: Option<NameOrigin>,
pub start: f32,
pub end: Option<f32>,
pub values: Vec<EventValue>,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EventValue {
pub name: String,
pub value: EventValueData,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum EventValueData {
I64(i64),
F64(f64),
Bool(bool),
Text(String),
Bytes(Vec<u8>),
}
impl Component for ClipEvents {
const NAME: &'static str = "core.clip_events";
const GEOMETRY: Geometry = Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
if !world.has::<Clip>(entity) {
return Err("core.clip_events must be on a core.clip".into());
}
if self.origin.as_ref().is_some_and(|s| s.trim().is_empty()) {
return Err("clip event origin is empty".into());
}
if self.refusals.iter().any(|s| s.trim().is_empty()) {
return Err("clip event refusal is empty".into());
}
for (i, event) in self.events.iter().enumerate() {
if !event.start.is_finite() || event.end.is_some_and(|end| !end.is_finite() || end < event.start)
{
return Err(format!(
"event {i}: start must be finite, end finite and at least start"
));
}
if event.name.is_some() != event.naming.is_some()
|| event.name.as_ref().is_some_and(|s| s.trim().is_empty())
{
return Err(format!(
"event {i}: name and naming origin must be present together and nonempty"
));
}
if let Some(naming) = &event.naming {
naming.verify().map_err(|reason| format!("event {i}: {reason}"))?;
}
for (at, field) in event.values.iter().enumerate() {
if field.name.trim().is_empty() {
return Err(format!("event {i} field {at}: name is empty"));
}
if matches!(field.value,EventValueData::F64(v) if !v.is_finite()) {
return Err(format!("event {i} field {at}: numeric value is not finite"));
}
}
}
Ok(())
}
fn apply_linear(&self, _linear: &Linear) -> Linearity<Self> {
Linearity::Invariant
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Extrapolation;
fn fixture() -> (World, Entity, ClipEvents) {
let mut world = World::new();
let entity = world.spawn();
world.insert(
entity,
Clip {
name: "clip".into(),
duration: 1.0,
extrapolation: Extrapolation::Hold,
channels: vec![],
root_motion: None,
additive: None,
},
);
let events = ClipEvents {
origin: Some("source event record".into()),
refusals: vec!["unknown behavior kept raw".into()],
events: vec![ClipEvent {
kind: 193,
name: Some("Opacity".into()),
naming: Some(NameOrigin::Community {
credit: "template author".into(),
}),
start: 0.25,
end: Some(2.0),
bytes: vec![0, 1, 255],
values: vec![
EventValue {
name: "value".into(),
value: EventValueData::I64(-2),
},
EventValue {
name: "value".into(),
value: EventValueData::F64(0.5),
},
EventValue {
name: "enabled".into(),
value: EventValueData::Bool(true),
},
EventValue {
name: "label".into(),
value: EventValueData::Text("source".into()),
},
EventValue {
name: "raw".into(),
value: EventValueData::Bytes(vec![255, 0]),
},
],
}],
};
(world, entity, events)
}
#[test]
fn all_typed_values_and_duplicate_labels_roundtrip_in_source_order() {
let (mut world, entity, events) = fixture();
assert!(
events.verify(entity, &world).is_ok(),
"explicit end may outlast the clip"
);
let value = events.to_json().unwrap().unwrap();
let copy: ClipEvents = serde_json::from_value(value).unwrap();
assert_eq!(copy, events);
assert_eq!(copy.events[0].values[0].name, copy.events[0].values[1].name);
world.insert(entity, events.clone());
let subset = world.subset(&[entity].into());
assert_eq!(subset.get::<ClipEvents>(entity), Some(&events));
assert!(
events.remap_primitives(&[2, 0, 2]).is_none(),
"events are not mesh-primitive data"
);
assert!(matches!(
events.apply_linear(&Linear::mirror_x()),
Linearity::Invariant
));
}
#[test]
fn a_start_before_the_clip_is_kept_as_stored() {
let (world, entity, events) = fixture();
for (start, end) in [(-1.0 / 30.0, Some(0.3)), (-0.5, None), (-0.25, Some(-0.25))] {
let mut e = events.clone();
e.events[0].start = start;
e.events[0].end = end;
assert!(e.verify(entity, &world).is_ok(), "{start}..{end:?}");
let copy: ClipEvents = serde_json::from_value(e.to_json().unwrap().unwrap()).unwrap();
assert_eq!(copy.events[0].start, start);
}
}
#[test]
fn malformed_times_names_and_nonfinite_fields_refuse() {
let (world, entity, events) = fixture();
for (start, end) in [
(f32::NAN, None),
(f32::NEG_INFINITY, Some(0.25)),
(0.5, Some(0.25)),
(-0.1, Some(-0.2)),
(0.0, Some(f32::INFINITY)),
] {
let mut e = events.clone();
e.events[0].start = start;
e.events[0].end = end;
assert!(e.verify(entity, &world).is_err());
}
let mut e = events.clone();
e.events[0].naming = None;
assert!(e.verify(entity, &world).is_err());
let mut e = events.clone();
e.events[0].name = None;
assert!(e.verify(entity, &world).is_err());
let mut e = events.clone();
e.events[0].naming = Some(NameOrigin::Community { credit: " ".into() });
assert!(e.verify(entity, &world).is_err());
let mut e = events.clone();
e.refusals.push(String::new());
assert!(e.verify(entity, &world).is_err());
let mut e = events.clone();
e.origin = Some(String::new());
assert!(e.verify(entity, &world).is_err());
let mut e = events.clone();
e.events[0].values[0].value = EventValueData::F64(f64::NAN);
assert!(e.verify(entity, &world).is_err());
assert!(e.to_json().is_err());
let mut e = events.clone();
e.events[0].end = None;
assert!(e.verify(entity, &world).is_ok());
assert!(e.verify(Entity::from_index(99), &world).is_err());
let mut value = events.to_json().unwrap().unwrap();
value["unknown"] = serde_json::json!(true);
assert!(serde_json::from_value::<ClipEvents>(value).is_err());
}
}