Skip to main content

primase_core/
clip_identity.rs

1//! Identity and an optional known length of a clip plan entity. No decoder recipe is World data.
2use crate::world::Linearity;
3use crate::{Component, Entity, Geometry, JsonError, Linear, World};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// `core.clip_key`: the reader's opaque stable identity, on a listed or loaded clip.
8/// A listed clip is a `core.asset` member with `Role::Clip` without `core.clip`.
9/// Its source descriptor belongs to the reader; loading preserves this entity and key.
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(transparent)]
12pub struct ClipKey(pub String);
13
14/// `core.clip_length`: seconds, present only when the listing knows the length.
15/// Absence means unknown, not zero. Once loaded, `core.clip.duration` is authoritative.
16#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
17#[serde(transparent)]
18pub struct ClipLength(pub f32);
19
20impl Component for ClipKey {
21    const NAME: &'static str = "core.clip_key";
22    const GEOMETRY: Geometry = Geometry::None;
23    fn to_json(&self) -> Result<Option<Value>, JsonError> {
24        crate::json::to_value(self).map(Some)
25    }
26    fn verify(&self, _entity: Entity, _world: &World) -> Result<(), String> {
27        if self.0.trim().is_empty() {
28            return Err("clip key is empty".into());
29        }
30        Ok(())
31    }
32    fn apply_linear(&self, _linear: &Linear) -> Linearity<Self> {
33        Linearity::Invariant
34    }
35}
36
37impl Component for ClipLength {
38    const NAME: &'static str = "core.clip_length";
39    const GEOMETRY: Geometry = Geometry::None;
40    fn to_json(&self) -> Result<Option<Value>, JsonError> {
41        crate::json::to_value(self).map(Some)
42    }
43    fn verify(&self, _entity: Entity, _world: &World) -> Result<(), String> {
44        if !self.0.is_finite() || self.0 < 0.0 {
45            return Err("clip length must be finite and non-negative seconds".into());
46        }
47        Ok(())
48    }
49    fn apply_linear(&self, _linear: &Linear) -> Linearity<Self> {
50        Linearity::Invariant
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    #[test]
58    fn plan_identity_and_known_length_roundtrip_before_loading() {
59        let mut world = World::new();
60        let entity = world.spawn();
61        let key = ClipKey(r#"{"member":"walk"}"#.into());
62        let length = ClipLength(2.5);
63        world.insert(entity, key.clone());
64        world.insert(entity, length);
65        assert!(world.verify_components().is_empty());
66        assert_eq!(
67            serde_json::from_value::<ClipKey>(key.to_json().unwrap().unwrap()).unwrap(),
68            key
69        );
70        assert_eq!(
71            serde_json::from_value::<ClipLength>(length.to_json().unwrap().unwrap()).unwrap(),
72            length
73        );
74        let part = world.subset(&[entity].into());
75        assert_eq!(part.get::<ClipKey>(entity), Some(&key));
76        assert_eq!(part.get::<ClipLength>(entity), Some(&length));
77        assert!(!world.has::<crate::Clip>(entity));
78        assert!(key.remap_primitives(&[1, 0]).is_none());
79        assert!(length.remap_primitives(&[1, 0]).is_none());
80        assert!(matches!(
81            key.apply_linear(&Linear::of(&crate::Transform::IDENTITY)),
82            Linearity::Invariant
83        ));
84        assert!(matches!(
85            length.apply_linear(&Linear::of(&crate::Transform::IDENTITY)),
86            Linearity::Invariant
87        ));
88    }
89    #[test]
90    fn invalid_identity_and_length_refuse() {
91        let mut world = World::new();
92        let e = world.spawn();
93        assert!(ClipKey(" ".into()).verify(e, &world).is_err());
94        for seconds in [f32::NAN, f32::INFINITY, -0.01] {
95            assert!(ClipLength(seconds).verify(e, &world).is_err());
96        }
97        assert!(ClipLength(0.0).verify(e, &world).is_ok());
98    }
99}