pub mod inherit;
use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SubclipId(pub u64);
impl std::fmt::Display for SubclipId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "subclip:{}", self.0)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct Subclip {
pub id: SubclipId,
pub parent_id: u64,
pub in_point: u64,
pub out_point: u64,
pub label: String,
pub color: [u8; 3],
}
impl Subclip {
#[allow(dead_code)]
#[must_use]
pub fn new(
id: SubclipId,
parent_id: u64,
in_point: u64,
out_point: u64,
label: impl Into<String>,
color: [u8; 3],
) -> Self {
Self {
id,
parent_id,
in_point,
out_point,
label: label.into(),
color,
}
}
#[allow(dead_code)]
#[must_use]
pub fn duration(&self) -> u64 {
self.out_point.saturating_sub(self.in_point)
}
}
#[allow(dead_code)]
pub struct SubclipTree {
children: HashMap<u64, Vec<Subclip>>,
}
impl SubclipTree {
#[allow(dead_code)]
#[must_use]
pub fn new() -> Self {
Self {
children: HashMap::new(),
}
}
#[allow(dead_code)]
pub fn add(&mut self, parent: u64, subclip: Subclip) {
self.children.entry(parent).or_default().push(subclip);
}
#[allow(dead_code)]
#[must_use]
pub fn children_of(&self, parent: u64) -> Vec<&Subclip> {
self.children
.get(&parent)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
#[allow(dead_code)]
#[must_use]
pub fn flatten(&self) -> Vec<&Subclip> {
self.children.values().flatten().collect()
}
#[allow(dead_code)]
#[must_use]
pub fn len(&self) -> usize {
self.children.values().map(|v| v.len()).sum()
}
#[allow(dead_code)]
#[must_use]
pub fn is_empty(&self) -> bool {
self.children.values().all(|v| v.is_empty())
}
}
impl Default for SubclipTree {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub struct SubclipValidator;
impl SubclipValidator {
#[allow(dead_code)]
pub fn validate(subclip: &Subclip, parent_duration: u64) -> Result<(), String> {
if subclip.in_point >= subclip.out_point {
return Err(format!(
"in_point ({}) must be less than out_point ({})",
subclip.in_point, subclip.out_point
));
}
if subclip.out_point > parent_duration {
return Err(format!(
"out_point ({}) exceeds parent duration ({})",
subclip.out_point, parent_duration
));
}
if subclip.label.is_empty() {
return Err("label must not be empty".to_string());
}
Ok(())
}
}
#[allow(dead_code)]
pub struct SubclipExporter;
impl SubclipExporter {
#[allow(dead_code)]
#[must_use]
pub fn to_edl_event(subclip: &Subclip, fps: f64) -> String {
let in_tc = frames_to_timecode(subclip.in_point, fps);
let out_tc = frames_to_timecode(subclip.out_point, fps);
format!(
"001 AX V C {} {} {} {}",
in_tc, out_tc, in_tc, out_tc
)
}
}
fn frames_to_timecode(frame: u64, fps: f64) -> String {
let fps_int = fps.round() as u64;
let fps_safe = fps_int.max(1);
let total_seconds = frame / fps_safe;
let ff = frame % fps_safe;
let ss = total_seconds % 60;
let mm = (total_seconds / 60) % 60;
let hh = total_seconds / 3600;
format!("{hh:02}:{mm:02}:{ss:02}:{ff:02}")
}
#[cfg(test)]
mod tests {
use super::*;
fn make_subclip(id: u64, parent: u64, in_pt: u64, out_pt: u64, label: &str) -> Subclip {
Subclip::new(SubclipId(id), parent, in_pt, out_pt, label, [255, 0, 0])
}
#[test]
fn test_subclip_duration() {
let sc = make_subclip(1, 10, 50, 150, "Scene A");
assert_eq!(sc.duration(), 100);
}
#[test]
fn test_subclip_duration_zero() {
let sc = make_subclip(1, 10, 100, 100, "Empty");
assert_eq!(sc.duration(), 0);
}
#[test]
fn test_subclip_id_display() {
let id = SubclipId(42);
assert_eq!(id.to_string(), "subclip:42");
}
#[test]
fn test_subclip_tree_add_and_children() {
let mut tree = SubclipTree::new();
tree.add(1, make_subclip(1, 1, 0, 100, "A"));
tree.add(1, make_subclip(2, 1, 100, 200, "B"));
tree.add(2, make_subclip(3, 2, 0, 50, "C"));
let children = tree.children_of(1);
assert_eq!(children.len(), 2);
let children2 = tree.children_of(2);
assert_eq!(children2.len(), 1);
}
#[test]
fn test_subclip_tree_flatten() {
let mut tree = SubclipTree::new();
tree.add(1, make_subclip(1, 1, 0, 100, "A"));
tree.add(2, make_subclip(2, 2, 0, 50, "B"));
let flat = tree.flatten();
assert_eq!(flat.len(), 2);
}
#[test]
fn test_subclip_tree_empty() {
let tree = SubclipTree::new();
assert!(tree.is_empty());
assert_eq!(tree.len(), 0);
assert_eq!(tree.children_of(99).len(), 0);
}
#[test]
fn test_subclip_tree_len() {
let mut tree = SubclipTree::new();
tree.add(1, make_subclip(1, 1, 0, 100, "A"));
tree.add(1, make_subclip(2, 1, 100, 200, "B"));
assert_eq!(tree.len(), 2);
assert!(!tree.is_empty());
}
#[test]
fn test_validator_valid() {
let sc = make_subclip(1, 10, 10, 50, "Good");
assert!(SubclipValidator::validate(&sc, 100).is_ok());
}
#[test]
fn test_validator_in_out_equal() {
let sc = make_subclip(1, 10, 50, 50, "Bad");
assert!(SubclipValidator::validate(&sc, 100).is_err());
}
#[test]
fn test_validator_out_exceeds_parent() {
let sc = make_subclip(1, 10, 10, 150, "Bad");
assert!(SubclipValidator::validate(&sc, 100).is_err());
}
#[test]
fn test_validator_empty_label() {
let sc = Subclip::new(SubclipId(1), 10, 10, 50, "", [0, 0, 0]);
assert!(SubclipValidator::validate(&sc, 100).is_err());
}
#[test]
fn test_edl_event_format() {
let sc = make_subclip(1, 10, 0, 25, "Test");
let edl = SubclipExporter::to_edl_event(&sc, 25.0);
assert!(edl.starts_with("001 AX V C "));
assert!(edl.contains("00:00:00:00"));
assert!(edl.contains("00:00:01:00"));
}
#[test]
fn test_frames_to_timecode() {
assert_eq!(frames_to_timecode(90, 30.0), "00:00:03:00");
assert_eq!(frames_to_timecode(25, 25.0), "00:00:01:00");
assert_eq!(frames_to_timecode(48, 24.0), "00:00:02:00");
}
}