use crate::entities::attr_schemas::PLAYER_SCHEMA;
use crate::entities::{Attrs, AttrValue, Node, Project};
use crate::entities::frame::Frame;
use log::{info, trace};
use serde::{Deserialize, Serialize};
use std::time::Instant;
use uuid::Uuid;
const FPS_PRESETS: &[f32] = &[1.0, 2.0, 4.0, 8.0, 12.0, 24.0, 30.0, 60.0, 120.0, 240.0, 480.0, 960.0];
pub const FRAME_JUMP_STEP: i32 = 25;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Player {
pub attrs: Attrs,
#[serde(skip)]
pub last_frame_time: Option<Instant>,
}
impl Player {
pub fn new() -> Self {
info!("Player initialized (project-less architecture)");
let mut attrs = Attrs::with_schema(&*PLAYER_SCHEMA);
attrs.set("is_playing", AttrValue::Bool(false));
attrs.set("fps_base", AttrValue::Float(24.0));
attrs.set("fps_play", AttrValue::Float(24.0));
attrs.set("loop_enabled", AttrValue::Bool(true));
attrs.set("play_direction", AttrValue::Float(1.0));
attrs.set_uuid_list("previous_comp_history", &[]);
Self {
attrs,
last_frame_time: None,
}
}
pub fn attach_schema(&mut self) {
self.attrs.attach_schema(&*PLAYER_SCHEMA);
}
pub fn active_comp(&self) -> Option<Uuid> {
self.attrs.get_uuid("active_comp")
}
fn set_active_comp_uuid(&mut self, uuid: Option<Uuid>) {
match uuid {
Some(id) => self.attrs.set_uuid("active_comp", id),
None => {
let _ = self.attrs.remove("active_comp");
}
}
}
pub fn previous_comp_history(&self) -> Vec<Uuid> {
self.attrs.get_uuid_list("previous_comp_history").unwrap_or_default()
}
pub fn take_previous_comp(&mut self) -> Option<Uuid> {
let mut history = self.previous_comp_history();
let prev = history.pop();
if prev.is_some() {
self.attrs.set_uuid_list("previous_comp_history", &history);
}
prev
}
fn push_previous_comp(&mut self, uuid: Uuid) {
const PREV_COMP_HISTORY_MAX: usize = 32;
let mut history = self.previous_comp_history();
if history.last().copied() == Some(uuid) {
return;
}
history.push(uuid);
if history.len() > PREV_COMP_HISTORY_MAX {
let overflow = history.len() - PREV_COMP_HISTORY_MAX;
history.drain(0..overflow);
}
self.attrs.set_uuid_list("previous_comp_history", &history);
}
pub fn is_playing(&self) -> bool {
self.attrs.get_bool_or("is_playing", false)
}
pub fn set_is_playing(&mut self, playing: bool) {
self.attrs.set("is_playing", AttrValue::Bool(playing));
}
pub fn fps_base(&self) -> f32 {
self.attrs.get_float_or("fps_base", 24.0)
}
pub fn set_fps_base(&mut self, fps: f32) {
self.attrs.set("fps_base", AttrValue::Float(fps));
}
pub fn fps_play(&self) -> f32 {
self.attrs.get_float_or("fps_play", 24.0)
}
pub fn set_fps_play(&mut self, fps: f32) {
self.attrs.set("fps_play", AttrValue::Float(fps));
}
pub fn loop_enabled(&self) -> bool {
self.attrs.get_bool_or("loop_enabled", true)
}
pub fn set_loop_enabled(&mut self, enabled: bool) {
self.attrs.set("loop_enabled", AttrValue::Bool(enabled));
}
pub fn play_direction(&self) -> f32 {
self.attrs.get_float_or("play_direction", 1.0)
}
fn set_play_direction(&mut self, dir: f32) {
self.attrs.set("play_direction", AttrValue::Float(dir));
}
pub fn selected_seq_idx(&self) -> Option<usize> {
self.attrs.get_i32("selected_seq_idx").map(|v| v.max(0) as usize)
}
pub fn set_selected_seq_idx(&mut self, idx: Option<usize>) {
match idx {
Some(v) => self.attrs.set("selected_seq_idx", AttrValue::Int(v as i32)),
None => {
let _ = self.attrs.remove("selected_seq_idx");
}
}
}
pub fn total_frames(&self, project: &Project) -> i32 {
self.active_comp()
.and_then(|uuid| project.with_node(uuid, |n| {
let (start, end) = n.play_range(true);
(end - start + 1).max(0)
}))
.unwrap_or(0)
}
pub fn play_range(&self, project: &Project) -> (i32, i32) {
self.active_comp()
.and_then(|uuid| project.with_node(uuid, |n| n.play_range(true)))
.unwrap_or((0, 0))
}
pub fn set_play_range(&mut self, start: i32, end: i32, project: &mut Project) {
if let Some(uuid) = self.active_comp() {
project.modify_comp(uuid, |comp| {
if comp._out() < comp._in() {
return;
}
let comp_start = comp._in();
let comp_end = comp._out();
let clamped_start = start.clamp(comp_start, comp_end);
let clamped_end = end.clamp(comp_start, comp_end);
let (final_start, final_end) = if clamped_end < clamped_start {
(clamped_end, clamped_start)
} else {
(clamped_start, clamped_end)
};
comp.set_comp_play_start(final_start);
comp.set_comp_play_end(final_end);
let (visible_start, visible_end) = comp.play_range(true);
let current = comp.frame();
if current < visible_start || current > visible_end {
comp.set_frame(visible_start);
}
});
}
}
pub fn current_frame(&self, project: &Project) -> i32 {
self.active_comp()
.and_then(|uuid| project.with_comp(uuid, |c| c.frame()))
.unwrap_or(0)
}
pub fn get_current_frame(&self, project: &Project) -> Option<Frame> {
let comp_uuid = self.active_comp()?;
let frame_idx = project.with_comp(comp_uuid, |c| c.frame())?;
project.compute_frame(comp_uuid, frame_idx)
}
pub fn set_active_comp(&mut self, comp_uuid: Option<Uuid>, project: &mut Project) {
let Some(uuid) = comp_uuid else {
self.set_active_comp_uuid(None);
project.set_active(None);
return;
};
if !project.contains_comp(uuid) {
log::warn!("Comp {} not found, cannot activate", uuid);
return;
}
let current = self.active_comp();
if current != Some(uuid) {
if let Some(current_uuid) = current {
self.push_previous_comp(current_uuid);
}
}
self.set_is_playing(false);
self.set_active_comp_uuid(Some(uuid));
project.set_active(Some(uuid));
project.modify_comp(uuid, |comp| {
comp.on_activate();
let frame = comp.frame();
comp.set_frame(frame);
log::info!("Activated comp {} at frame {}", uuid, frame);
});
project.set_selection(vec![uuid]);
}
pub fn update(&mut self, project: &mut Project) -> Option<i32> {
if !self.is_playing() || self.total_frames(project) == 0 {
return None;
}
let fps_base = self.fps_base();
let fps_play = self.fps_play();
if fps_play < fps_base {
self.set_fps_play(fps_base);
}
let now = Instant::now();
if let Some(last_time) = self.last_frame_time {
let elapsed = now.duration_since(last_time).as_secs_f32();
let frame_duration = 1.0 / self.fps_play();
if elapsed >= frame_duration {
let new_frame = self.advance_frame(project);
self.last_frame_time = Some(now);
return new_frame;
}
} else {
self.last_frame_time = Some(now);
}
None
}
fn advance_frame(&mut self, project: &mut Project) -> Option<i32> {
let total_frames = self.total_frames(project);
if total_frames == 0 {
return None;
}
let (play_start, play_end) = self.play_range(project);
if play_end < play_start {
return None;
}
let play_direction = self.play_direction();
let loop_enabled = self.loop_enabled();
let mut should_stop = false;
let mut new_frame: Option<i32> = None;
if let Some(uuid) = self.active_comp() {
project.modify_comp(uuid, |comp| {
let mut current = comp.frame();
if current < play_start || current > play_end {
current = if play_direction >= 0.0 {
play_start
} else {
play_end
};
comp.set_frame(current);
}
if play_direction > 0.0 {
let next = current + 1;
if next > play_end {
if loop_enabled {
trace!("Frame loop: {} -> {}", current, play_start);
comp.set_frame(play_start);
new_frame = Some(play_start);
} else {
trace!("Reached play range end, stopping");
comp.set_frame(play_end);
new_frame = Some(play_end);
should_stop = true;
}
} else {
comp.set_frame(next);
new_frame = Some(next);
}
} else {
if current <= play_start {
if loop_enabled {
trace!("Frame loop: {} -> {}", current, play_end);
comp.set_frame(play_end);
new_frame = Some(play_end);
} else {
trace!("Reached play range start, stopping");
should_stop = true;
}
} else {
comp.set_frame(current - 1);
new_frame = Some(current - 1);
}
}
});
}
if should_stop {
self.set_is_playing(false);
}
new_frame
}
pub fn stop(&mut self) {
if self.is_playing() {
self.set_is_playing(false);
trace!("Playback stopped");
self.last_frame_time = None;
self.set_fps_play(self.fps_base());
}
}
pub fn to_start(&mut self, project: &mut Project) {
let (start, _) = self.play_range(project);
trace!("Rewinding to frame {}", start);
if let Some(uuid) = self.active_comp() {
project.modify_comp(uuid, |comp| {
comp.set_frame(start);
});
}
self.last_frame_time = None;
}
pub fn to_end(&mut self, project: &mut Project) {
let (_, end) = self.play_range(project);
trace!("Skipping to end: frame {}", end);
if let Some(uuid) = self.active_comp() {
project.modify_comp(uuid, |comp| {
comp.set_frame(end);
});
}
self.last_frame_time = None;
}
pub fn set_frame(&mut self, frame: i32, project: &mut Project) {
if let Some(uuid) = self.active_comp() {
project.modify_comp(uuid, |comp| {
let comp_start = comp._in();
let comp_end = comp._out();
if comp_end < comp_start {
return;
}
let clamped = frame.clamp(comp_start, comp_end);
comp.set_frame(clamped);
});
}
self.last_frame_time = None;
}
pub fn step(&mut self, count: i32, project: &mut Project) {
if count == 0 {
return;
}
let current = self.current_frame(project);
let (play_start, play_end) = self.play_range(project);
let loop_enabled = self.loop_enabled();
let target = (i64::from(current) + i64::from(count))
.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32;
let range_size = play_end - play_start + 1;
if range_size <= 0 {
return;
}
let final_frame = if target > play_end {
if loop_enabled {
let overflow = target - play_end;
play_start + ((overflow - 1) % range_size)
} else {
play_end
}
} else if target < play_start {
if loop_enabled {
let underflow = play_start - target;
play_end - ((underflow - 1) % range_size)
} else {
play_start
}
} else {
target
};
if let Some(uuid) = self.active_comp() {
project.modify_comp(uuid, |comp| {
comp.set_frame(final_frame);
});
}
}
fn start_jog(&mut self, direction: f32) {
if !self.is_playing() {
self.set_play_direction(direction);
self.set_is_playing(true);
self.set_fps_play(self.fps_base()); self.last_frame_time = Some(Instant::now());
} else if self.play_direction().signum() != direction.signum() {
self.set_play_direction(direction);
self.set_fps_play(self.fps_base()); } else {
self.increase_fps_play();
}
}
pub fn jog_forward(&mut self) {
self.start_jog(1.0);
}
pub fn jog_backward(&mut self) {
self.start_jog(-1.0);
}
pub fn increase_fps_base(&mut self) {
let fps_base = self.fps_base();
if let Some(idx) = FPS_PRESETS.iter().position(|&f| f > fps_base) {
let new_fps = FPS_PRESETS[idx];
self.set_fps_base(new_fps);
self.set_fps_play(new_fps); trace!("Base FPS increased to {}", new_fps);
}
}
pub fn decrease_fps_base(&mut self) {
let fps_base = self.fps_base();
if let Some(idx) = FPS_PRESETS.iter().rposition(|&f| f < fps_base) {
let new_fps = FPS_PRESETS[idx];
self.set_fps_base(new_fps);
self.set_fps_play(new_fps); trace!("Base FPS decreased to {}", new_fps);
}
}
fn increase_fps_play(&mut self) {
let fps_play = self.fps_play();
if let Some(idx) = FPS_PRESETS.iter().position(|&f| f >= fps_play)
&& idx + 1 < FPS_PRESETS.len()
{
let new_fps = FPS_PRESETS[idx + 1];
self.set_fps_play(new_fps);
trace!("Play FPS increased to {}", new_fps);
}
}
pub fn reset_settings(&mut self) {
self.set_fps_base(24.0);
self.set_fps_play(24.0);
self.set_loop_enabled(true);
info!("Player settings reset");
}
}
impl Default for Player {
fn default() -> Self {
Self::new()
}
}