#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ReelId(pub u32);
impl std::fmt::Display for ReelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Reel-{:04}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct ReelClip {
pub clip_id: String,
pub reel_id: ReelId,
pub position_frames: u64,
pub duration_frames: u64,
}
impl ReelClip {
#[must_use]
pub fn new(
clip_id: impl Into<String>,
reel_id: ReelId,
position_frames: u64,
duration_frames: u64,
) -> Self {
Self {
clip_id: clip_id.into(),
reel_id,
position_frames,
duration_frames,
}
}
#[must_use]
pub fn end_frame(&self) -> u64 {
self.position_frames + self.duration_frames
}
}
#[derive(Debug, Clone)]
pub struct Reel {
pub id: ReelId,
pub name: String,
pub duration_frames: u64,
pub fps: f32,
pub resolution: (u32, u32),
pub clips: Vec<ReelClip>,
}
impl Reel {
#[must_use]
pub fn new(
id: ReelId,
name: impl Into<String>,
duration_frames: u64,
fps: f32,
resolution: (u32, u32),
) -> Self {
Self {
id,
name: name.into(),
duration_frames,
fps,
resolution,
clips: Vec::new(),
}
}
#[must_use]
pub fn duration_secs(&self) -> f64 {
if self.fps <= 0.0 {
return 0.0;
}
self.duration_frames as f64 / f64::from(self.fps)
}
#[must_use]
pub fn clip_at_frame(&self, frame: u64) -> Option<&ReelClip> {
self.clips
.iter()
.find(|c| frame >= c.position_frames && frame < c.end_frame())
}
pub fn add_clip(&mut self, clip: ReelClip) {
self.clips.push(clip);
self.clips.sort_by_key(|c| c.position_frames);
}
}
#[derive(Debug)]
pub struct ReelSet {
pub reels: Vec<Reel>,
pub project_name: String,
}
impl ReelSet {
#[must_use]
pub fn new(project_name: impl Into<String>) -> Self {
Self {
reels: Vec::new(),
project_name: project_name.into(),
}
}
#[must_use]
pub fn total_duration_frames(&self) -> u64 {
self.reels.iter().map(|r| r.duration_frames).sum()
}
#[must_use]
pub fn reel_count(&self) -> usize {
self.reels.len()
}
#[must_use]
pub fn find_reel(&self, id: ReelId) -> Option<&Reel> {
self.reels.iter().find(|r| r.id == id)
}
pub fn add_reel(&mut self, reel: Reel) {
self.reels.push(reel);
}
}
#[derive(Debug, Clone)]
pub struct ReelIssue {
pub reel_id: ReelId,
pub issue_type: ReelIssueType,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReelIssueType {
Gap,
Overlap,
NamingConflict,
DurationMismatch,
}
pub struct ReelValidator;
impl ReelValidator {
#[must_use]
pub fn check(reels: &[Reel]) -> Vec<ReelIssue> {
let mut issues = Vec::new();
for reel in reels {
issues.extend(Self::check_reel(reel));
}
issues.extend(Self::check_naming_conflicts(reels));
issues
}
fn check_reel(reel: &Reel) -> Vec<ReelIssue> {
let mut issues = Vec::new();
let mut sorted = reel.clips.clone();
sorted.sort_by_key(|c| c.position_frames);
for i in 0..sorted.len() {
let clip = &sorted[i];
if clip.end_frame() > reel.duration_frames {
issues.push(ReelIssue {
reel_id: reel.id,
issue_type: ReelIssueType::DurationMismatch,
description: format!(
"Clip '{}' ends at frame {} but reel duration is {}",
clip.clip_id,
clip.end_frame(),
reel.duration_frames
),
});
}
if i + 1 < sorted.len() {
let next = &sorted[i + 1];
if next.position_frames > clip.end_frame() {
issues.push(ReelIssue {
reel_id: reel.id,
issue_type: ReelIssueType::Gap,
description: format!(
"Gap between clips '{}' and '{}': frames {}–{}",
clip.clip_id,
next.clip_id,
clip.end_frame(),
next.position_frames
),
});
}
if next.position_frames < clip.end_frame() {
issues.push(ReelIssue {
reel_id: reel.id,
issue_type: ReelIssueType::Overlap,
description: format!(
"Clips '{}' and '{}' overlap at frame {}",
clip.clip_id, next.clip_id, next.position_frames
),
});
}
}
}
issues
}
fn check_naming_conflicts(reels: &[Reel]) -> Vec<ReelIssue> {
let mut issues = Vec::new();
let mut seen: std::collections::HashMap<&str, ReelId> = std::collections::HashMap::new();
for reel in reels {
if let Some(&existing_id) = seen.get(reel.name.as_str()) {
issues.push(ReelIssue {
reel_id: reel.id,
issue_type: ReelIssueType::NamingConflict,
description: format!(
"Reel '{}' (id={}) shares name with reel id={}",
reel.name, reel.id, existing_id
),
});
} else {
seen.insert(&reel.name, reel.id);
}
}
issues
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_reel(id: u32, name: &str, duration: u64) -> Reel {
Reel::new(ReelId(id), name, duration, 24.0, (1920, 1080))
}
fn make_clip(clip_id: &str, reel_id: u32, pos: u64, dur: u64) -> ReelClip {
ReelClip::new(clip_id, ReelId(reel_id), pos, dur)
}
#[test]
fn test_reel_id_display() {
let id = ReelId(3);
assert_eq!(format!("{id}"), "Reel-0003");
}
#[test]
fn test_reel_duration_secs() {
let reel = make_reel(1, "A001", 240);
assert!((reel.duration_secs() - 10.0).abs() < 0.001);
}
#[test]
fn test_clip_end_frame() {
let clip = make_clip("clip1", 1, 100, 50);
assert_eq!(clip.end_frame(), 150);
}
#[test]
fn test_clip_at_frame_found() {
let mut reel = make_reel(1, "A001", 500);
reel.add_clip(make_clip("c1", 1, 0, 100));
reel.add_clip(make_clip("c2", 1, 100, 100));
assert_eq!(
reel.clip_at_frame(50).map(|c| c.clip_id.as_str()),
Some("c1")
);
assert_eq!(
reel.clip_at_frame(100).map(|c| c.clip_id.as_str()),
Some("c2")
);
}
#[test]
fn test_clip_at_frame_not_found() {
let reel = make_reel(1, "A001", 500);
assert!(reel.clip_at_frame(300).is_none());
}
#[test]
fn test_reel_set_total_duration() {
let mut rs = ReelSet::new("TestProject");
rs.add_reel(make_reel(1, "A001", 240));
rs.add_reel(make_reel(2, "A002", 360));
assert_eq!(rs.total_duration_frames(), 600);
}
#[test]
fn test_reel_set_find_reel() {
let mut rs = ReelSet::new("Proj");
rs.add_reel(make_reel(1, "A001", 100));
assert!(rs.find_reel(ReelId(1)).is_some());
assert!(rs.find_reel(ReelId(99)).is_none());
}
#[test]
fn test_validator_no_issues() {
let mut reel = make_reel(1, "A001", 500);
reel.add_clip(make_clip("c1", 1, 0, 100));
reel.add_clip(make_clip("c2", 1, 100, 200));
let issues = ReelValidator::check(&[reel]);
assert!(issues.is_empty(), "unexpected issues: {issues:?}");
}
#[test]
fn test_validator_detects_gap() {
let mut reel = make_reel(1, "A001", 500);
reel.add_clip(make_clip("c1", 1, 0, 50));
reel.add_clip(make_clip("c2", 1, 100, 50));
let issues = ReelValidator::check(&[reel]);
assert!(issues.iter().any(|i| i.issue_type == ReelIssueType::Gap));
}
#[test]
fn test_validator_detects_overlap() {
let mut reel = make_reel(1, "A001", 500);
reel.add_clip(make_clip("c1", 1, 0, 100));
reel.add_clip(make_clip("c2", 1, 50, 100)); let issues = ReelValidator::check(&[reel]);
assert!(issues
.iter()
.any(|i| i.issue_type == ReelIssueType::Overlap));
}
#[test]
fn test_validator_detects_duration_mismatch() {
let mut reel = make_reel(1, "A001", 100);
reel.add_clip(make_clip("c1", 1, 80, 50)); let issues = ReelValidator::check(&[reel]);
assert!(issues
.iter()
.any(|i| i.issue_type == ReelIssueType::DurationMismatch));
}
#[test]
fn test_validator_detects_naming_conflict() {
let r1 = make_reel(1, "A001", 100);
let r2 = make_reel(2, "A001", 200); let issues = ReelValidator::check(&[r1, r2]);
assert!(issues
.iter()
.any(|i| i.issue_type == ReelIssueType::NamingConflict));
}
}