#[derive(Debug, Clone)]
pub struct StoryboardFrame {
pub frame_idx: u64,
pub is_key_frame: bool,
pub thumbnail_path: String,
pub notes: String,
}
impl StoryboardFrame {
#[must_use]
pub fn new(
frame_idx: u64,
is_key_frame: bool,
thumbnail_path: impl Into<String>,
notes: impl Into<String>,
) -> Self {
Self {
frame_idx,
is_key_frame,
thumbnail_path: thumbnail_path.into(),
notes: notes.into(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.thumbnail_path.is_empty() && self.notes.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct StoryboardPanel {
pub shot_id: u64,
pub frames: Vec<StoryboardFrame>,
pub direction: String,
}
impl StoryboardPanel {
#[must_use]
pub fn new(shot_id: u64, direction: impl Into<String>) -> Self {
Self {
shot_id,
frames: Vec::new(),
direction: direction.into(),
}
}
#[must_use]
pub fn frame_count(&self) -> usize {
self.frames.len()
}
#[must_use]
pub fn key_frames(&self) -> Vec<&StoryboardFrame> {
self.frames.iter().filter(|f| f.is_key_frame).collect()
}
pub fn add_frame(&mut self, frame: StoryboardFrame) {
self.frames.push(frame);
}
}
#[derive(Debug, Clone)]
pub struct Storyboard {
pub title: String,
pub panels: Vec<StoryboardPanel>,
pub created_ms: u64,
}
impl Storyboard {
#[must_use]
pub fn new(title: impl Into<String>, created_ms: u64) -> Self {
Self {
title: title.into(),
panels: Vec::new(),
created_ms,
}
}
pub fn add_panel(&mut self, panel: StoryboardPanel) {
self.panels.push(panel);
}
#[must_use]
pub fn panel_count(&self) -> usize {
self.panels.len()
}
#[must_use]
pub fn total_frames(&self) -> usize {
self.panels.iter().map(StoryboardPanel::frame_count).sum()
}
#[must_use]
pub fn export_summary(&self) -> String {
format!(
"Storyboard: {}\nPanels: {}\nTotal frames: {}\nCreated: {}ms",
self.title,
self.panel_count(),
self.total_frames(),
self.created_ms,
)
}
}
pub struct KeyFrameSelector;
impl KeyFrameSelector {
#[must_use]
pub fn select(total_frames: u64, n_keyframes: usize) -> Vec<u64> {
if n_keyframes == 0 || total_frames == 0 {
return Vec::new();
}
let n = n_keyframes as u64;
if n >= total_frames {
return (0..total_frames).collect();
}
(0..n)
.map(|i| i * (total_frames - 1) / (n - 1).max(1))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_is_empty() {
let f = StoryboardFrame::new(0, false, "", "");
assert!(f.is_empty());
}
#[test]
fn test_frame_not_empty_thumbnail() {
let thumb = std::env::temp_dir()
.join("oximedia-scene-storyboard-thumb.jpg")
.to_string_lossy()
.into_owned();
let f = StoryboardFrame::new(0, false, thumb, "");
assert!(!f.is_empty());
}
#[test]
fn test_frame_not_empty_notes() {
let f = StoryboardFrame::new(0, false, "", "Close-up of hero");
assert!(!f.is_empty());
}
#[test]
fn test_panel_frame_count() {
let mut panel = StoryboardPanel::new(0, "Wide shot");
panel.add_frame(StoryboardFrame::new(10, true, "", ""));
panel.add_frame(StoryboardFrame::new(20, false, "", ""));
assert_eq!(panel.frame_count(), 2);
}
#[test]
fn test_panel_key_frames() {
let mut panel = StoryboardPanel::new(1, "Action");
panel.add_frame(StoryboardFrame::new(0, true, "", ""));
panel.add_frame(StoryboardFrame::new(5, false, "", ""));
panel.add_frame(StoryboardFrame::new(10, true, "", ""));
let kf = panel.key_frames();
assert_eq!(kf.len(), 2);
assert_eq!(kf[0].frame_idx, 0);
assert_eq!(kf[1].frame_idx, 10);
}
#[test]
fn test_panel_no_key_frames() {
let mut panel = StoryboardPanel::new(2, "");
panel.add_frame(StoryboardFrame::new(1, false, "", ""));
assert!(panel.key_frames().is_empty());
}
#[test]
fn test_storyboard_panel_count() {
let mut sb = Storyboard::new("Test", 0);
sb.add_panel(StoryboardPanel::new(0, ""));
sb.add_panel(StoryboardPanel::new(1, ""));
assert_eq!(sb.panel_count(), 2);
}
#[test]
fn test_storyboard_total_frames() {
let mut sb = Storyboard::new("T", 0);
let mut p1 = StoryboardPanel::new(0, "");
p1.add_frame(StoryboardFrame::new(0, true, "", ""));
p1.add_frame(StoryboardFrame::new(1, false, "", ""));
let mut p2 = StoryboardPanel::new(1, "");
p2.add_frame(StoryboardFrame::new(10, true, "", ""));
sb.add_panel(p1);
sb.add_panel(p2);
assert_eq!(sb.total_frames(), 3);
}
#[test]
fn test_storyboard_total_frames_empty() {
let sb = Storyboard::new("Empty", 0);
assert_eq!(sb.total_frames(), 0);
}
#[test]
fn test_storyboard_export_summary_title() {
let sb = Storyboard::new("My Film", 12345);
let s = sb.export_summary();
assert!(s.contains("My Film"));
}
#[test]
fn test_storyboard_export_summary_counts() {
let mut sb = Storyboard::new("X", 0);
let mut p = StoryboardPanel::new(0, "");
p.add_frame(StoryboardFrame::new(0, true, "", ""));
sb.add_panel(p);
let s = sb.export_summary();
assert!(s.contains('1')); }
#[test]
fn test_keyframe_selector_basic() {
let indices = KeyFrameSelector::select(100, 5);
assert_eq!(indices.len(), 5);
assert_eq!(*indices.first().expect("should succeed in test"), 0);
assert_eq!(*indices.last().expect("should succeed in test"), 99);
}
#[test]
fn test_keyframe_selector_zero_n() {
let indices = KeyFrameSelector::select(100, 0);
assert!(indices.is_empty());
}
#[test]
fn test_keyframe_selector_zero_frames() {
let indices = KeyFrameSelector::select(0, 5);
assert!(indices.is_empty());
}
#[test]
fn test_keyframe_selector_n_exceeds_frames() {
let indices = KeyFrameSelector::select(3, 10);
assert_eq!(indices.len(), 3);
assert_eq!(indices, vec![0, 1, 2]);
}
#[test]
fn test_keyframe_selector_single() {
let indices = KeyFrameSelector::select(50, 1);
assert_eq!(indices.len(), 1);
assert_eq!(indices[0], 0);
}
}