Skip to main content

primase_core/
clip_transition.rs

1//! Source-stated incoming transition timing, independent of any event numbering or engine rule.
2use crate::{Component, Entity, Geometry, JsonError, Linear, World};
3use serde::{Deserialize, Serialize};
4
5/// A source pass's timing override for transitions into a clip.
6/// `None` retains the caller's authored duration; a refusal must never be treated as an absent override.
7#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
8pub struct IncomingTransition {
9    pub duration: Option<f64>,
10    pub refusal: Option<String>,
11}
12
13/// A caller's authored duration and the optional source override that resolved it.
14#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
15pub struct EffectBlend {
16    pub authored: f64,
17    pub event_end: Option<f64>,
18}
19impl EffectBlend {
20    pub fn seconds(&self) -> f64 {
21        self.event_end.unwrap_or(self.authored)
22    }
23}
24impl IncomingTransition {
25    /// Resolve a stated duration using only this typed source result, never by interpreting clip events.
26    pub fn resolve(&self, authored: f64) -> Result<EffectBlend, String> {
27        if !authored.is_finite() || authored < 0.0 {
28            return Err(format!(
29                "the effect's authored duration {authored} is not seconds"
30            ));
31        }
32        if let Some(reason) = &self.refusal {
33            return Err(reason.clone());
34        }
35        Ok(EffectBlend {
36            authored,
37            event_end: self.duration,
38        })
39    }
40}
41impl Component for IncomingTransition {
42    const NAME: &'static str = "core.incoming_transition";
43    const GEOMETRY: Geometry = Geometry::None;
44    fn verify(&self, _: Entity, _: &World) -> Result<(), String> {
45        if self
46            .duration
47            .is_some_and(|value| !value.is_finite() || value < 0.)
48        {
49            return Err("incoming transition duration must be finite and non-negative".into());
50        }
51        if self
52            .refusal
53            .as_ref()
54            .is_some_and(|reason| reason.trim().is_empty())
55        {
56            return Err("empty incoming transition refusal".into());
57        }
58        if self.duration.is_some() && self.refusal.is_some() {
59            return Err("incoming transition cannot be both resolved and refused".into());
60        }
61        Ok(())
62    }
63    fn to_json(&self) -> Result<Option<serde_json::Value>, JsonError> {
64        crate::json::to_value(self).map(Some)
65    }
66    fn apply_linear(&self, _: &Linear) -> crate::Linearity<Self> {
67        crate::Linearity::Invariant
68    }
69}