Skip to main content

alpha2_eval/
error.rs

1use alpha2_media::SourceId;
2use alpha2_model::{
3    ClipBlendMode, ClipEffect, ClipId, CompositionId, ProjectId, TrackId, TrackKind,
4    VisualProperties,
5};
6use alpha2_time::TimeRange;
7use std::fmt;
8
9/// An evaluator failure.
10#[derive(Clone, Debug, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum EvalError {
13    /// The input project violated a model invariant.
14    InvalidProject(alpha2_model::ModelError),
15    /// A valid range could not be represented during integer retiming.
16    UnmappableClip {
17        /// Clip whose source timestamp could not be calculated.
18        clip: ClipId,
19    },
20    /// A visible clip's composition cannot draw its output (see [`crate::PlanError`]).
21    IncompleteComposition {
22        /// The clip showing the composition.
23        clip: ClipId,
24        /// The composition that failed to plan.
25        composition: CompositionId,
26        /// What is missing.
27        reason: crate::PlanError,
28    },
29    /// The reusable index belongs to a different project.
30    IndexProjectMismatch {
31        /// Project identity captured when the index was built.
32        index_project: ProjectId,
33        /// Project identity passed to evaluation.
34        evaluated_project: ProjectId,
35    },
36}
37
38impl std::error::Error for EvalError {
39    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40        match self {
41            Self::InvalidProject(error) => Some(error),
42            _ => None,
43        }
44    }
45}
46
47impl fmt::Display for EvalError {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::InvalidProject(error) => {
51                write!(formatter, "cannot index invalid project: {error}")
52            }
53            Self::UnmappableClip { clip } => {
54                write!(
55                    formatter,
56                    "clip {clip} cannot be mapped to a source timestamp"
57                )
58            }
59            Self::IncompleteComposition {
60                clip,
61                composition,
62                reason,
63            } => match reason {
64                crate::PlanError::NoOutput => write!(
65                    formatter,
66                    "composition {composition} of clip {clip} has no output"
67                ),
68                crate::PlanError::MissingInput { node, input } => write!(
69                    formatter,
70                    "input {input} of node {node} in composition {composition} of clip {clip} is not connected"
71                ),
72                crate::PlanError::UnboundGroupInput { node } => write!(
73                    formatter,
74                    "group input {node} in composition {composition} of clip {clip} is fed by no group node"
75                ),
76                crate::PlanError::ClipWithoutClip { node } => write!(
77                    formatter,
78                    "clip node {node} in composition {composition}, drawn as clip {clip}'s source, shows no clip"
79                ),
80                crate::PlanError::NestedTooDeep { node } => write!(
81                    formatter,
82                    "node {node} in composition {composition} of clip {clip} nests compositions more than {} deep",
83                    alpha2_model::MAX_COMPOSITION_DEPTH
84                ),
85                crate::PlanError::TooLarge => write!(
86                    formatter,
87                    "composition {composition} of clip {clip} draws more than {} nodes, or the project's compositions hold more than {} bytes once inlined",
88                    alpha2_model::MAX_INLINED_NODES,
89                    crate::MAX_PLAN_BYTES
90                ),
91            },
92            Self::IndexProjectMismatch {
93                index_project,
94                evaluated_project,
95            } => {
96                write!(
97                    formatter,
98                    "index belongs to project {index_project}, not {evaluated_project}"
99                )
100            }
101        }
102    }
103}
104
105#[derive(Clone, Debug)]
106pub(crate) struct IndexedClip {
107    pub(crate) clip_id: ClipId,
108    pub(crate) source_id: SourceId,
109    pub(crate) track_id: TrackId,
110    pub(crate) track_kind: TrackKind,
111    pub(crate) timeline_range: TimeRange,
112    pub(crate) source_range: TimeRange,
113    pub(crate) time_map: alpha2_model::TimeMap,
114    pub(crate) visual: VisualProperties,
115    pub(crate) composition: Option<crate::IndexedComposition>,
116    pub(crate) blend_mode: ClipBlendMode,
117    pub(crate) cache: alpha2_model::CacheIntent,
118    pub(crate) masks: Vec<alpha2_model::ShapeMask>,
119    pub(crate) matte: Option<alpha2_model::Matte>,
120    /// Whether another clip names this one as its matte.
121    pub(crate) is_matte: bool,
122    pub(crate) audio: alpha2_model::AudioProperties,
123    pub(crate) source_is_audible: bool,
124    pub(crate) generator: Option<alpha2_model::Generator>,
125    /// A shapes or layout generator's tree, planned once per source.
126    pub(crate) plan: Option<std::sync::Arc<crate::GeneratorPlan>>,
127    /// For a composition source, its composition compiled to draw it.
128    pub(crate) source_composition: Option<(alpha2_model::CompositionId, crate::index::PlanSlot)>,
129    pub(crate) track_order: usize,
130    pub(crate) clip_order: usize,
131}
132
133#[derive(Clone, Debug)]
134pub(crate) struct IndexedTransition {
135    pub(crate) from_clip: ClipId,
136    pub(crate) to_clip: ClipId,
137    pub(crate) timeline_range: TimeRange,
138    pub(crate) easing: alpha2_model::Easing,
139    pub(crate) effect: ClipEffect,
140    pub(crate) track_order: usize,
141}
142
143#[derive(Clone, Debug)]
144pub(crate) struct IndexedCaptions {
145    pub(crate) track_id: TrackId,
146    pub(crate) track_order: usize,
147    pub(crate) captions: alpha2_model::Captions,
148    /// Resolved cue styles, the track's base style first.
149    pub(crate) styles: Vec<alpha2_model::TextStyle>,
150    /// Index into `styles` per cue.
151    pub(crate) cue_styles: Vec<usize>,
152}
153
154impl IndexedCaptions {
155    pub(crate) fn new(
156        track_id: TrackId,
157        track_order: usize,
158        captions: alpha2_model::Captions,
159    ) -> Self {
160        let mut styles = vec![captions.style.clone()];
161        let mut by_name = std::collections::BTreeMap::<&str, usize>::new();
162        let mut cue_styles = Vec::with_capacity(captions.cues.len());
163        for cue in &captions.cues {
164            // A cue's own style name wins over its speaker's.
165            let name = cue.style.as_deref().or_else(|| {
166                cue.speaker
167                    .as_deref()
168                    .filter(|speaker| captions.styles.contains_key(*speaker))
169            });
170            let index = match name.and_then(|name| captions.styles.get_key_value(name)) {
171                Some((name, patch)) => *by_name.entry(name.as_str()).or_insert_with(|| {
172                    styles.push(patch.apply(&captions.style));
173                    styles.len() - 1
174                }),
175                None => 0,
176            };
177            cue_styles.push(index);
178        }
179        Self {
180            track_id,
181            track_order,
182            captions,
183            styles,
184            cue_styles,
185        }
186    }
187}