#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameRange {
pub start: u64,
pub end: u64,
}
impl FrameRange {
#[must_use]
pub fn new(start: u64, end: u64) -> Self {
Self { start, end }
}
#[must_use]
pub fn duration(&self) -> u64 {
self.end.saturating_sub(self.start)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.end <= self.start
}
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
self.start < other.end && other.start < self.end
}
#[must_use]
pub fn contains_frame(&self, frame: u64) -> bool {
frame >= self.start && frame < self.end
}
#[must_use]
pub fn intersection(&self, other: &Self) -> Option<Self> {
let start = self.start.max(other.start);
let end = self.end.min(other.end);
if start < end {
Some(Self { start, end })
} else {
None
}
}
#[must_use]
pub fn union(&self, other: &Self) -> Option<Self> {
if self.overlaps(other) || self.end == other.start || other.end == self.start {
Some(Self {
start: self.start.min(other.start),
end: self.end.max(other.end),
})
} else {
None
}
}
}
impl PartialOrd for FrameRange {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FrameRange {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.start.cmp(&other.start).then(self.end.cmp(&other.end))
}
}
#[derive(Debug, Clone)]
pub struct TimelineEvent {
pub event_id: u32,
pub record_in: u64,
pub record_out: u64,
pub label: Option<String>,
}
impl TimelineEvent {
#[must_use]
pub fn new(event_id: u32, record_in: u64, record_out: u64) -> Self {
Self {
event_id,
record_in,
record_out,
label: None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn range(&self) -> FrameRange {
FrameRange::new(self.record_in, self.record_out)
}
#[must_use]
pub fn duration(&self) -> u64 {
self.range().duration()
}
}
#[derive(Debug, Clone)]
pub struct TimelineGap {
pub range: FrameRange,
pub before_event: u32,
pub after_event: u32,
}
#[derive(Debug, Clone)]
pub struct TimelineOverlap {
pub range: FrameRange,
pub event_a: u32,
pub event_b: u32,
}
pub struct TimelineAnalyzer {
events: Vec<TimelineEvent>,
}
impl TimelineAnalyzer {
#[must_use]
pub fn new() -> Self {
Self { events: Vec::new() }
}
pub fn add_event(&mut self, event: TimelineEvent) {
self.events.push(event);
}
pub fn add_events(&mut self, events: impl IntoIterator<Item = TimelineEvent>) {
self.events.extend(events);
}
fn sorted_events(&self) -> Vec<&TimelineEvent> {
let mut sorted: Vec<&TimelineEvent> = self.events.iter().collect();
sorted.sort_by_key(|e| e.record_in);
sorted
}
#[must_use]
pub fn find_gaps(&self) -> Vec<TimelineGap> {
let sorted = self.sorted_events();
let mut gaps = Vec::new();
for window in sorted.windows(2) {
let prev = window[0];
let next = window[1];
if prev.record_out < next.record_in {
gaps.push(TimelineGap {
range: FrameRange::new(prev.record_out, next.record_in),
before_event: prev.event_id,
after_event: next.event_id,
});
}
}
gaps
}
#[must_use]
pub fn find_overlaps(&self) -> Vec<TimelineOverlap> {
let sorted = self.sorted_events();
let mut overlaps = Vec::new();
for i in 0..sorted.len() {
for j in (i + 1)..sorted.len() {
let a = sorted[i];
let b = sorted[j];
if b.record_in >= a.record_out {
break;
}
let overlap_start = b.record_in;
let overlap_end = a.record_out.min(b.record_out);
if overlap_start < overlap_end {
overlaps.push(TimelineOverlap {
range: FrameRange::new(overlap_start, overlap_end),
event_a: a.event_id,
event_b: b.event_id,
});
}
}
}
overlaps
}
#[must_use]
pub fn total_coverage_frames(&self) -> u64 {
let merged = self.merged_ranges();
merged.iter().map(|r| r.duration()).sum()
}
#[must_use]
pub fn merged_ranges(&self) -> Vec<FrameRange> {
if self.events.is_empty() {
return Vec::new();
}
let mut ranges: Vec<FrameRange> = self.events.iter().map(|e| e.range()).collect();
ranges.sort();
let mut merged: Vec<FrameRange> = Vec::new();
let mut current = ranges[0];
for &r in &ranges[1..] {
if let Some(u) = current.union(&r) {
current = u;
} else {
merged.push(current);
current = r;
}
}
merged.push(current);
merged
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn coverage_percentage(&self, total_range: &FrameRange) -> f64 {
if total_range.is_empty() {
return 0.0;
}
let covered = self.total_coverage_frames();
covered as f64 / total_range.duration() as f64 * 100.0
}
#[must_use]
pub fn event_count(&self) -> usize {
self.events.len()
}
pub fn clear(&mut self) {
self.events.clear();
}
}
impl Default for TimelineAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_range_basic() {
let r = FrameRange::new(0, 100);
assert_eq!(r.duration(), 100);
assert!(!r.is_empty());
}
#[test]
fn test_frame_range_empty() {
let r = FrameRange::new(100, 50);
assert!(r.is_empty());
assert_eq!(r.duration(), 0);
}
#[test]
fn test_frame_range_overlaps() {
let a = FrameRange::new(0, 100);
let b = FrameRange::new(50, 150);
assert!(a.overlaps(&b));
assert!(b.overlaps(&a));
}
#[test]
fn test_frame_range_no_overlap() {
let a = FrameRange::new(0, 50);
let b = FrameRange::new(50, 100);
assert!(!a.overlaps(&b));
}
#[test]
fn test_frame_range_contains_frame() {
let r = FrameRange::new(10, 20);
assert!(r.contains_frame(10));
assert!(r.contains_frame(15));
assert!(!r.contains_frame(20));
assert!(!r.contains_frame(9));
}
#[test]
fn test_frame_range_intersection() {
let a = FrameRange::new(0, 100);
let b = FrameRange::new(50, 150);
let inter = a.intersection(&b).expect("intersection should succeed");
assert_eq!(inter.start, 50);
assert_eq!(inter.end, 100);
}
#[test]
fn test_frame_range_intersection_none() {
let a = FrameRange::new(0, 50);
let b = FrameRange::new(50, 100);
assert!(a.intersection(&b).is_none());
}
#[test]
fn test_frame_range_union() {
let a = FrameRange::new(0, 50);
let b = FrameRange::new(50, 100);
let u = a.union(&b).expect("union should succeed");
assert_eq!(u.start, 0);
assert_eq!(u.end, 100);
}
#[test]
fn test_frame_range_union_disjoint() {
let a = FrameRange::new(0, 40);
let b = FrameRange::new(60, 100);
assert!(a.union(&b).is_none());
}
#[test]
fn test_timeline_event_creation() {
let ev = TimelineEvent::new(1, 0, 125);
assert_eq!(ev.event_id, 1);
assert_eq!(ev.duration(), 125);
assert!(ev.label.is_none());
}
#[test]
fn test_timeline_event_with_label() {
let ev = TimelineEvent::new(1, 0, 100).with_label("Shot A");
assert_eq!(ev.label.as_deref(), Some("Shot A"));
}
#[test]
fn test_find_gaps() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 150, 250));
let gaps = analyzer.find_gaps();
assert_eq!(gaps.len(), 1);
assert_eq!(gaps[0].range.start, 100);
assert_eq!(gaps[0].range.end, 150);
assert_eq!(gaps[0].before_event, 1);
assert_eq!(gaps[0].after_event, 2);
}
#[test]
fn test_find_no_gaps() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 100, 200));
let gaps = analyzer.find_gaps();
assert!(gaps.is_empty());
}
#[test]
fn test_find_overlaps() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 80, 180));
let overlaps = analyzer.find_overlaps();
assert_eq!(overlaps.len(), 1);
assert_eq!(overlaps[0].range.start, 80);
assert_eq!(overlaps[0].range.end, 100);
}
#[test]
fn test_find_no_overlaps() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 100, 200));
let overlaps = analyzer.find_overlaps();
assert!(overlaps.is_empty());
}
#[test]
fn test_total_coverage_no_overlap() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 100, 200));
assert_eq!(analyzer.total_coverage_frames(), 200);
}
#[test]
fn test_total_coverage_with_overlap() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 50, 150));
assert_eq!(analyzer.total_coverage_frames(), 150);
}
#[test]
fn test_coverage_percentage() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 50));
let total = FrameRange::new(0, 100);
let pct = analyzer.coverage_percentage(&total);
assert!((pct - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_merged_ranges() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
analyzer.add_event(TimelineEvent::new(2, 50, 150));
analyzer.add_event(TimelineEvent::new(3, 200, 300));
let merged = analyzer.merged_ranges();
assert_eq!(merged.len(), 2);
assert_eq!(merged[0], FrameRange::new(0, 150));
assert_eq!(merged[1], FrameRange::new(200, 300));
}
#[test]
fn test_analyzer_clear() {
let mut analyzer = TimelineAnalyzer::new();
analyzer.add_event(TimelineEvent::new(1, 0, 100));
assert_eq!(analyzer.event_count(), 1);
analyzer.clear();
assert_eq!(analyzer.event_count(), 0);
}
#[test]
fn test_analyzer_default() {
let analyzer = TimelineAnalyzer::default();
assert_eq!(analyzer.event_count(), 0);
}
}