mod blueprint_ext;
mod command;
mod db;
use std::collections::BTreeMap;
use re_chunk::TimelineName;
use re_log_types::{
AbsoluteTimeRange, AbsoluteTimeRangeF, Duration, TimeCell, TimeInt, TimeReal, TimeType,
Timeline,
};
use re_sdk_types::blueprint::components::{LoopMode, PlayState};
use crate::NeedsRepaint;
use crate::blueprint_helpers::BlueprintContext;
use blueprint_ext::TimeBlueprintExt as _;
pub use blueprint_ext::{TIME_PANEL_PATH, time_panel_blueprint_entity_path};
pub use command::{MoveDirection, MoveSpeed, TimeControlCommand};
pub use db::{PreviewRecordingsDb, TimeControlDb};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BufferBehavior {
Play,
WaitForDataThenPlay,
AlwaysBuffer,
}
impl BufferBehavior {
fn pauses_on_buffer(self) -> bool {
match self {
Self::Play => false,
Self::WaitForDataThenPlay | Self::AlwaysBuffer => true,
}
}
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
pub struct TimeView {
pub min: TimeReal,
pub time_spanned: f64,
}
impl From<AbsoluteTimeRange> for TimeView {
fn from(value: AbsoluteTimeRange) -> Self {
Self {
min: value.min().into(),
time_spanned: value.abs_length() as f64,
}
}
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
struct TimeState {
time: TimeReal,
#[serde(skip)]
last_paused_time: Option<TimeReal>,
fps: f32,
#[serde(default)]
time_selection: Option<AbsoluteTimeRangeF>,
#[serde(default)]
view: Option<TimeView>,
}
impl re_byte_size::SizeBytes for TimeState {
#[inline]
fn heap_size_bytes(&self) -> u64 {
0
}
#[inline]
fn is_pod() -> bool {
true
}
}
impl TimeState {
fn new(time: impl Into<TimeReal>) -> Self {
Self {
time: time.into(),
last_paused_time: None,
fps: 30.0, time_selection: Default::default(),
view: None,
}
}
}
#[derive(serde::Deserialize, serde::Serialize, Clone, PartialEq, Debug)]
enum ActiveTimeline {
Auto(Timeline),
UserEdited(Timeline),
Pending(TimelineName),
}
impl ActiveTimeline {
pub fn name(&self) -> &TimelineName {
match self {
Self::Auto(timeline) | Self::UserEdited(timeline) => timeline.name(),
Self::Pending(timeline_name) => timeline_name,
}
}
pub fn timeline(&self) -> Option<&Timeline> {
match self {
Self::Auto(timeline) | Self::UserEdited(timeline) => Some(timeline),
Self::Pending(_) => None,
}
}
}
#[derive(Clone, PartialEq)]
pub struct TimeControl {
timeline: ActiveTimeline,
states: BTreeMap<TimelineName, TimeState>,
playing: bool,
following: bool,
buffer_behavior: BufferBehavior,
speed: f32,
loop_mode: LoopMode,
pub highlighted_range: Option<AbsoluteTimeRange>,
}
impl Default for TimeControl {
fn default() -> Self {
Self {
timeline: ActiveTimeline::Auto(Timeline::pick_best_timeline([], |_| 0)),
states: Default::default(),
playing: true,
following: true,
buffer_behavior: BufferBehavior::WaitForDataThenPlay,
speed: 1.0,
loop_mode: LoopMode::Off,
highlighted_range: None,
}
}
}
impl re_byte_size::SizeBytes for TimeControl {
fn heap_size_bytes(&self) -> u64 {
let Self {
timeline: _,
states,
playing: _,
following: _,
buffer_behavior: _,
speed: _,
loop_mode: _,
highlighted_range: _,
} = self;
states.heap_size_bytes()
}
}
pub struct TimeControlUpdateParams {
pub stable_dt: f32,
pub more_data_is_streaming_in: bool,
pub is_buffering: bool,
pub should_diff_state: bool,
}
#[must_use]
pub struct TimeControlResponse {
pub needs_repaint: NeedsRepaint,
pub playing_change: Option<bool>,
pub timeline_change: Option<(Timeline, TimeReal)>,
pub time_change: Option<TimeReal>,
}
impl TimeControlResponse {
fn no_repaint() -> Self {
Self::new(NeedsRepaint::No)
}
fn new(needs_repaint: NeedsRepaint) -> Self {
Self {
needs_repaint,
playing_change: None,
timeline_change: None,
time_change: None,
}
}
}
impl TimeControl {
pub fn preview_time_control() -> Self {
Self {
playing: true,
following: false,
loop_mode: LoopMode::All,
buffer_behavior: BufferBehavior::AlwaysBuffer,
..Self::default()
}
}
fn start_buffering(&mut self) {
if self.buffer_behavior != BufferBehavior::AlwaysBuffer {
self.buffer_behavior = BufferBehavior::WaitForDataThenPlay;
}
}
pub fn from_blueprint(blueprint_ctx: &impl BlueprintContext) -> Self {
let mut this = Self::default();
this.update_from_blueprint(blueprint_ctx, None);
this
}
pub fn update_from_blueprint(
&mut self,
blueprint_ctx: &impl BlueprintContext,
db: Option<&dyn TimeControlDb>,
) {
if let Some(timeline) = blueprint_ctx.timeline() {
if matches!(self.timeline, ActiveTimeline::Auto(_))
|| timeline.as_str() != self.timeline_name().as_str()
{
self.timeline = ActiveTimeline::Pending(timeline);
}
} else if let Some(timeline) = self.timeline() {
self.timeline = ActiveTimeline::Auto(*timeline);
}
let old_timeline = *self.timeline_name();
if let Some(db) = db {
self.select_valid_timeline(db);
}
if let Some(new_play_state) = blueprint_ctx.play_state()
&& new_play_state != self.play_state()
{
self.set_play_state(db, new_play_state, Some(blueprint_ctx));
}
if let Some(new_loop_mode) = blueprint_ctx.loop_mode() {
self.loop_mode = new_loop_mode;
if self.loop_mode != LoopMode::Off {
if self.play_state() == PlayState::Following {
self.set_play_state(db, PlayState::Playing, Some(blueprint_ctx));
}
self.following = false;
}
}
if let Some(playback_speed) = blueprint_ctx.playback_speed() {
self.speed = playback_speed as f32;
}
let play_state = self.play_state();
let timeline = *self.timeline_name();
if let Some(state) = self.states.get_mut(&timeline) {
if let Some(fps) = blueprint_ctx.fps() {
state.fps = fps as f32;
}
let bp_loop_section = blueprint_ctx.time_selection();
if old_timeline == timeline {
state.time_selection = bp_loop_section.map(|r| r.into());
} else {
match state.time_selection {
Some(selection) => blueprint_ctx.set_time_selection(selection.to_int()),
None => {
blueprint_ctx.clear_time_selection();
}
}
}
match play_state {
PlayState::Paused => {
state.last_paused_time = Some(state.time);
}
PlayState::Playing | PlayState::Following => {}
}
}
}
pub fn set_time_ad_hoc(&mut self, time: TimeReal) {
self.set_time_cursor_ad_hoc(*self.timeline_name(), time);
}
pub fn set_time_cursor_ad_hoc(&mut self, timeline: TimelineName, time: TimeReal) {
self.states
.entry(timeline)
.or_insert_with(|| TimeState::new(time))
.time = time;
}
pub fn update(
&mut self,
db: &dyn TimeControlDb,
params: &TimeControlUpdateParams,
blueprint_ctx: Option<&impl BlueprintContext>,
) -> TimeControlResponse {
let TimeControlUpdateParams {
stable_dt,
more_data_is_streaming_in,
is_buffering,
should_diff_state,
} = *params;
let (old_playing, old_timeline, old_state) = (
self.playing,
self.timeline().copied(),
self.states.get(self.timeline_name()).copied(),
);
if let Some(blueprint_ctx) = blueprint_ctx {
self.update_from_blueprint(blueprint_ctx, Some(db));
} else {
self.select_valid_timeline(db);
}
let Some(full_range) = db.time_range_for(self.timeline_name()) else {
return TimeControlResponse::no_repaint(); };
let needs_repaint = match self.play_state() {
PlayState::Paused => {
let state = self.states.entry(*self.timeline_name()).or_insert_with(|| {
TimeState::new(if self.following {
full_range.max()
} else {
full_range.min()
})
});
state.last_paused_time = Some(state.time);
self.start_buffering(); NeedsRepaint::No
}
PlayState::Playing => {
let state = self
.states
.entry(*self.timeline_name())
.or_insert_with(|| TimeState::new(full_range.min()));
if self.buffer_behavior.pauses_on_buffer() && is_buffering {
NeedsRepaint::No
} else {
if self.buffer_behavior == BufferBehavior::WaitForDataThenPlay {
self.buffer_behavior = BufferBehavior::Play;
}
let dt = stable_dt.min(0.1) * self.speed;
if self.loop_mode == LoopMode::Off && full_range.max() <= state.time {
self.set_time_ad_hoc(full_range.max().into());
if more_data_is_streaming_in {
} else {
self.pause(blueprint_ctx);
}
NeedsRepaint::No
} else {
let mut new_time = state.time;
let loop_range = match self.loop_mode {
LoopMode::Off => None,
LoopMode::Selection => state.time_selection,
LoopMode::All => Some(full_range.into()),
};
match self.timeline.timeline().map(|t| t.typ()) {
Some(TimeType::Sequence) => {
new_time += TimeReal::from(state.fps * dt);
}
Some(TimeType::DurationNs | TimeType::TimestampNs) => {
new_time += TimeReal::from(Duration::from_secs(dt));
}
None => {}
}
if let Some(loop_range) = loop_range
&& loop_range.max < new_time
{
new_time = loop_range.min; }
self.set_time_ad_hoc(new_time);
NeedsRepaint::Yes
}
}
}
PlayState::Following => {
self.set_time_ad_hoc(full_range.max().into());
NeedsRepaint::No }
};
self.apply_state_diff_if_needed(
TimeControlResponse::new(needs_repaint),
should_diff_state,
db,
old_timeline,
old_playing,
old_state,
)
}
#[expect(clippy::fn_params_excessive_bools)] fn apply_state_diff_if_needed(
&mut self,
response: TimeControlResponse,
should_diff_state: bool,
db: &dyn TimeControlDb,
old_timeline: Option<Timeline>,
old_playing: bool,
old_state: Option<TimeState>,
) -> TimeControlResponse {
let mut response = response;
if should_diff_state && db.time_range_for(self.timeline_name()).is_some() {
self.diff_with(&mut response, old_timeline, old_playing, old_state);
}
response
}
fn diff_with(
&mut self,
response: &mut TimeControlResponse,
old_timeline: Option<Timeline>,
old_playing: bool,
old_state: Option<TimeState>,
) {
if old_playing != self.playing {
response.playing_change = Some(self.playing);
}
if old_timeline != self.timeline().copied() {
let time = self
.time_for_timeline(*self.timeline_name())
.unwrap_or(TimeReal::MIN);
response.timeline_change = self.timeline().map(|t| (*t, time));
}
if let Some(state) = self.states.get_mut(self.timeline.name()) {
if old_state.is_none_or(|old_state| old_state.time != state.time) {
response.time_change = Some(state.time);
}
}
}
pub fn play_state(&self) -> PlayState {
if self.playing {
if self.following {
PlayState::Following
} else {
PlayState::Playing
}
} else {
PlayState::Paused
}
}
pub fn loop_mode(&self) -> LoopMode {
if self.play_state() == PlayState::Following {
LoopMode::Off
} else {
self.loop_mode
}
}
pub fn set_play_state(
&mut self,
db: Option<&dyn TimeControlDb>,
play_state: PlayState,
blueprint_ctx: Option<&impl BlueprintContext>,
) {
if let Some(blueprint_ctx) = blueprint_ctx
&& Some(play_state) != blueprint_ctx.play_state()
{
blueprint_ctx.set_play_state(play_state);
}
match play_state {
PlayState::Paused => {
self.playing = false;
}
PlayState::Playing => {
self.playing = true;
self.following = false;
self.start_buffering();
if let Some(db) = db
&& let Some(range) = db.time_range_for(self.timeline_name())
{
if let Some(state) = self.states.get_mut(self.timeline.name()) {
if range.max <= state.time {
state.time = range.min.into();
}
} else {
self.states
.insert(*self.timeline_name(), TimeState::new(range.min));
}
}
}
PlayState::Following => {
self.playing = true;
self.following = true;
if let Some(db) = db
&& let Some(range) = db.time_range_for(self.timeline_name())
{
self.states
.entry(*self.timeline_name())
.or_insert_with(|| TimeState::new(range.max))
.time = range.max.into();
}
}
}
}
fn pause(&mut self, blueprint_ctx: Option<&impl BlueprintContext>) {
self.playing = false;
if let Some(blueprint_ctx) = blueprint_ctx {
blueprint_ctx.set_play_state(PlayState::Paused);
}
if let Some(state) = self.states.get_mut(self.timeline.name()) {
state.last_paused_time = Some(state.time);
}
}
pub fn time_cursor(&self) -> Option<re_entity_db::PrefetchTimeCursor> {
let typ = self.time_type()?;
let speed_if_unpaused = match typ {
TimeType::DurationNs | TimeType::TimestampNs => {
TimeInt::from_secs(1.0).as_f64() * self.speed as f64
}
TimeType::Sequence => self.fps()? as f64 * self.speed as f64,
};
let loop_range = if self.loop_mode == LoopMode::All {
Some(AbsoluteTimeRange::new(TimeInt::MIN, TimeInt::MAX))
} else {
self.active_loop_selection().map(|r| r.to_int())
};
Some(re_entity_db::PrefetchTimeCursor {
time_cursor: re_log_types::TimelinePoint {
name: *self.timeline_name(),
typ,
time: self.time_int()?,
},
speed_if_unpaused,
loop_range,
})
}
pub fn speed(&self) -> f32 {
self.speed
}
pub fn fps(&self) -> Option<f32> {
self.states.get(self.timeline_name()).map(|state| state.fps)
}
fn select_valid_timeline(&mut self, db: &dyn TimeControlDb) {
let timelines = db.timelines();
let reset_timeline = match &self.timeline {
ActiveTimeline::Auto(_) => true,
ActiveTimeline::UserEdited(selected) => !timelines.contains_key(selected.name()),
ActiveTimeline::Pending(timeline_name) => {
if let Some(timeline) = timelines.get(timeline_name) {
self.timeline = ActiveTimeline::UserEdited(*timeline);
}
false
}
};
if reset_timeline || matches!(self.timeline, ActiveTimeline::Auto(_)) {
self.timeline =
ActiveTimeline::Auto(Timeline::pick_best_timeline(timelines.values(), |t| {
db.num_temporal_rows_on_timeline(t.name())
}));
}
}
#[inline]
pub fn timeline(&self) -> Option<&Timeline> {
self.timeline.timeline()
}
pub fn timeline_name(&self) -> &TimelineName {
self.timeline.name()
}
pub fn time_type(&self) -> Option<TimeType> {
self.timeline().map(|t| t.typ())
}
pub fn time(&self) -> Option<TimeReal> {
self.states
.get(self.timeline_name())
.map(|state| state.time)
}
pub fn last_paused_time(&self) -> Option<TimeReal> {
if matches!(self.play_state(), PlayState::Paused) {
self.time()
} else {
self.states
.get(self.timeline_name())
.and_then(|state| state.last_paused_time)
}
}
pub fn time_cell(&self) -> Option<TimeCell> {
let t = self.time()?;
Some(TimeCell::new(self.time_type()?, t.floor().as_i64()))
}
pub fn time_int(&self) -> Option<TimeInt> {
self.time().map(|t| t.floor())
}
pub fn time_i64(&self) -> Option<i64> {
self.time().map(|t| t.floor().as_i64())
}
pub fn current_query(&self) -> re_chunk_store::LatestAtQuery {
re_chunk_store::LatestAtQuery::new(
*self.timeline_name(),
self.time().map_or(TimeInt::MAX, |t| t.floor()),
)
}
pub fn active_loop_selection(&self) -> Option<AbsoluteTimeRangeF> {
if self.loop_mode == LoopMode::Selection {
self.states.get(self.timeline_name())?.time_selection
} else {
None
}
}
pub fn time_selection(&self) -> Option<AbsoluteTimeRangeF> {
self.states.get(self.timeline_name())?.time_selection
}
pub fn is_time_selected(&self, timeline: &TimelineName, needle: TimeInt) -> bool {
if timeline != self.timeline_name() {
return false;
}
if let Some(state) = self.states.get(self.timeline_name()) {
state.time.floor() == needle
} else {
false
}
}
pub fn is_pending(&self) -> bool {
matches!(self.timeline, ActiveTimeline::Pending(_))
}
pub fn time_for_timeline(&self, timeline: TimelineName) -> Option<TimeReal> {
self.states.get(&timeline).map(|state| state.time)
}
pub fn time_view(&self) -> Option<TimeView> {
self.states
.get(self.timeline_name())
.and_then(|state| state.view)
}
}