#![allow(dead_code)]
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransitionType {
Dissolve,
AddDissolve,
SmpteWipe(u16),
DipToColor,
Custom(Uuid),
}
impl TransitionType {
#[must_use]
pub fn label(&self) -> String {
match self {
Self::Dissolve => "Dissolve".to_string(),
Self::AddDissolve => "Additive Dissolve".to_string(),
Self::SmpteWipe(code) => format!("SMPTE Wipe {code}"),
Self::DipToColor => "Dip to Color".to_string(),
Self::Custom(id) => format!("Custom ({id})"),
}
}
#[must_use]
pub const fn is_dissolve(&self) -> bool {
matches!(self, Self::Dissolve | Self::AddDissolve)
}
#[must_use]
pub const fn is_wipe(&self) -> bool {
matches!(self, Self::SmpteWipe(_))
}
}
impl std::fmt::Display for TransitionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.label())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TransitionDef {
id: Uuid,
transition_type: TransitionType,
duration: i64,
edit_rate_num: u32,
edit_rate_den: u32,
name: Option<String>,
reversed: bool,
}
impl TransitionDef {
#[must_use]
pub fn new(
transition_type: TransitionType,
duration: i64,
edit_rate_num: u32,
edit_rate_den: u32,
) -> Self {
Self {
id: Uuid::new_v4(),
transition_type,
duration,
edit_rate_num,
edit_rate_den,
name: None,
reversed: false,
}
}
#[must_use]
pub fn with_id(mut self, id: Uuid) -> Self {
self.id = id;
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn reversed(mut self) -> Self {
self.reversed = true;
self
}
#[must_use]
pub fn id(&self) -> Uuid {
self.id
}
#[must_use]
pub fn transition_type(&self) -> &TransitionType {
&self.transition_type
}
#[must_use]
pub fn duration(&self) -> i64 {
self.duration
}
#[must_use]
pub fn edit_rate(&self) -> (u32, u32) {
(self.edit_rate_num, self.edit_rate_den)
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn is_reversed(&self) -> bool {
self.reversed
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn duration_seconds(&self) -> f64 {
if self.edit_rate_den == 0 || self.edit_rate_num == 0 {
return 0.0;
}
let rate = self.edit_rate_num as f64 / self.edit_rate_den as f64;
self.duration as f64 / rate
}
}
#[derive(Debug, Clone)]
pub struct TransitionCatalog {
entries: HashMap<Uuid, TransitionDef>,
}
impl TransitionCatalog {
#[must_use]
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn insert(&mut self, def: TransitionDef) -> Option<TransitionDef> {
self.entries.insert(def.id(), def)
}
pub fn remove(&mut self, id: &Uuid) -> Option<TransitionDef> {
self.entries.remove(id)
}
#[must_use]
pub fn get(&self, id: &Uuid) -> Option<&TransitionDef> {
self.entries.get(id)
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn find_by_type(&self, tt: &TransitionType) -> Vec<&TransitionDef> {
self.entries
.values()
.filter(|d| &d.transition_type == tt)
.collect()
}
#[must_use]
pub fn dissolves(&self) -> Vec<&TransitionDef> {
self.entries
.values()
.filter(|d| d.transition_type.is_dissolve())
.collect()
}
#[must_use]
pub fn wipes(&self) -> Vec<&TransitionDef> {
self.entries
.values()
.filter(|d| d.transition_type.is_wipe())
.collect()
}
pub fn iter(&self) -> impl Iterator<Item = &TransitionDef> {
self.entries.values()
}
}
impl Default for TransitionCatalog {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transition_type_label() {
assert_eq!(TransitionType::Dissolve.label(), "Dissolve");
assert_eq!(TransitionType::AddDissolve.label(), "Additive Dissolve");
assert_eq!(TransitionType::SmpteWipe(1).label(), "SMPTE Wipe 1");
assert_eq!(TransitionType::DipToColor.label(), "Dip to Color");
}
#[test]
fn test_transition_type_is_dissolve() {
assert!(TransitionType::Dissolve.is_dissolve());
assert!(TransitionType::AddDissolve.is_dissolve());
assert!(!TransitionType::SmpteWipe(4).is_dissolve());
assert!(!TransitionType::DipToColor.is_dissolve());
}
#[test]
fn test_transition_type_is_wipe() {
assert!(TransitionType::SmpteWipe(1).is_wipe());
assert!(!TransitionType::Dissolve.is_wipe());
}
#[test]
fn test_transition_type_display() {
let t = TransitionType::SmpteWipe(42);
assert_eq!(format!("{t}"), "SMPTE Wipe 42");
}
#[test]
fn test_transition_def_creation() {
let def = TransitionDef::new(TransitionType::Dissolve, 30, 30, 1);
assert_eq!(*def.transition_type(), TransitionType::Dissolve);
assert_eq!(def.duration(), 30);
assert_eq!(def.edit_rate(), (30, 1));
assert!(!def.is_reversed());
assert!(def.name().is_none());
}
#[test]
fn test_transition_def_builders() {
let id = Uuid::new_v4();
let def = TransitionDef::new(TransitionType::SmpteWipe(1), 15, 25, 1)
.with_id(id)
.with_name("wipe_lr")
.reversed();
assert_eq!(def.id(), id);
assert_eq!(def.name(), Some("wipe_lr"));
assert!(def.is_reversed());
}
#[test]
fn test_transition_def_duration_seconds() {
let def = TransitionDef::new(TransitionType::Dissolve, 50, 25, 1);
let dur = def.duration_seconds();
assert!((dur - 2.0).abs() < 1e-9);
}
#[test]
fn test_transition_def_duration_zero_rate() {
let def = TransitionDef::new(TransitionType::Dissolve, 50, 0, 0);
assert_eq!(def.duration_seconds(), 0.0);
}
#[test]
fn test_catalog_insert_get_remove() {
let mut cat = TransitionCatalog::new();
let def = TransitionDef::new(TransitionType::Dissolve, 30, 25, 1);
let id = def.id();
cat.insert(def);
assert_eq!(cat.len(), 1);
assert!(cat.get(&id).is_some());
assert!(cat.remove(&id).is_some());
assert!(cat.is_empty());
}
#[test]
fn test_catalog_find_by_type() {
let mut cat = TransitionCatalog::new();
cat.insert(TransitionDef::new(TransitionType::Dissolve, 30, 25, 1));
cat.insert(TransitionDef::new(TransitionType::SmpteWipe(1), 15, 25, 1));
cat.insert(TransitionDef::new(TransitionType::Dissolve, 20, 25, 1));
let dissolves = cat.find_by_type(&TransitionType::Dissolve);
assert_eq!(dissolves.len(), 2);
}
#[test]
fn test_catalog_dissolves_and_wipes() {
let mut cat = TransitionCatalog::new();
cat.insert(TransitionDef::new(TransitionType::Dissolve, 30, 25, 1));
cat.insert(TransitionDef::new(TransitionType::AddDissolve, 20, 25, 1));
cat.insert(TransitionDef::new(TransitionType::SmpteWipe(1), 15, 25, 1));
cat.insert(TransitionDef::new(TransitionType::SmpteWipe(4), 10, 25, 1));
cat.insert(TransitionDef::new(TransitionType::DipToColor, 25, 25, 1));
assert_eq!(cat.dissolves().len(), 2);
assert_eq!(cat.wipes().len(), 2);
}
#[test]
fn test_catalog_iter() {
let mut cat = TransitionCatalog::new();
cat.insert(TransitionDef::new(TransitionType::Dissolve, 30, 25, 1));
cat.insert(TransitionDef::new(TransitionType::SmpteWipe(1), 15, 25, 1));
assert_eq!(cat.iter().count(), 2);
}
#[test]
fn test_catalog_default() {
let cat = TransitionCatalog::default();
assert!(cat.is_empty());
}
#[test]
fn test_transition_type_custom() {
let custom_id = Uuid::new_v4();
let t = TransitionType::Custom(custom_id);
assert!(!t.is_dissolve());
assert!(!t.is_wipe());
assert!(t.label().contains("Custom"));
}
}