use re_log_types::{AbsoluteTimeRange, AbsoluteTimeRangeF, TimeReal, TimeType};
use re_sdk_types::encodings::{TimeInt, TimeRange, TimeRangeBoundary};
use re_viewer_context::{TimeControlCommand, ViewerContext};
pub fn resolve_time_axis_range(
view_range: &TimeRange,
timeline_range: AbsoluteTimeRange,
cursor: TimeInt,
) -> AbsoluteTimeRange {
let min = match view_range.start {
TimeRangeBoundary::Infinite => timeline_range.min.as_i64(),
_ => view_range.start.start_boundary_time(cursor).0,
};
let max = match view_range.end {
TimeRangeBoundary::Infinite => timeline_range.max.as_i64(),
_ => view_range.end.end_boundary_time(cursor).0,
};
AbsoluteTimeRange::new(min, max)
}
pub fn cursor_centered_default_range(time_type: TimeType, data_span: u64) -> Option<TimeRange> {
const NS_PER_SEC: i64 = 1_000_000_000;
match time_type {
TimeType::Sequence => (2_000 < data_span).then(|| TimeRange::from_cursor_plus_minus(1_000)),
TimeType::TimestampNs | TimeType::DurationNs => ((60 * NS_PER_SEC as u64) < data_span)
.then(|| TimeRange::from_cursor_plus_minus(30 * NS_PER_SEC)),
}
}
pub fn time_axis_range_after_cursor_move(
view_range: &TimeRange,
from: i64,
to: i64,
) -> Option<TimeRange> {
let time_diff = from - to;
let mut any_relative = false;
let mut map_boundary = |boundary| {
if let TimeRangeBoundary::CursorRelative(offset) = boundary {
any_relative = true;
TimeRangeBoundary::CursorRelative((offset.0 + time_diff).into())
} else {
boundary
}
};
let shifted = TimeRange {
start: map_boundary(view_range.start),
end: map_boundary(view_range.end),
};
any_relative.then_some(shifted)
}
pub fn set_time_cursor(
ctx: &ViewerContext<'_>,
current_time: Option<i64>,
view_range: Option<&TimeRange>,
time: TimeReal,
) -> Option<TimeRange> {
let time = ctx
.recording()
.time_range_for(ctx.time_ctrl.timeline_name())
.map_or(time, |timeline_range| {
let timeline_range = AbsoluteTimeRangeF::from(timeline_range);
time.clamp(timeline_range.min, timeline_range.max)
});
let new_view_range = current_time.and_then(|current_time| {
let view_range = view_range?;
time_axis_range_after_cursor_move(view_range, current_time, time.floor().as_i64())
});
ctx.send_time_commands([
TimeControlCommand::SetTimeClamped(time),
TimeControlCommand::Pause,
]);
new_view_range
}
pub fn time_axis_time_from_plot(value: TimeReal, time_offset: i64) -> TimeInt {
let rounded: TimeInt = value.round().into();
TimeInt(rounded.0.saturating_add(time_offset))
}
pub fn recover_relative_boundaries_after_zoom_or_pan(
min: TimeInt,
max: TimeInt,
previous: &TimeRange,
cursor: TimeInt,
) -> TimeRange {
let boundary = |value: TimeInt, previous| match previous {
TimeRangeBoundary::CursorRelative(_) => value
.0
.checked_sub(cursor.0)
.map_or(TimeRangeBoundary::Absolute(value), |offset| {
TimeRangeBoundary::CursorRelative(TimeInt(offset))
}),
TimeRangeBoundary::Infinite | TimeRangeBoundary::Absolute(_) => {
TimeRangeBoundary::Absolute(value)
}
};
TimeRange {
start: boundary(min, previous.start),
end: boundary(max, previous.end),
}
}
#[cfg(test)]
mod tests {
use super::*;
use TimeRangeBoundary::{Absolute, CursorRelative, Infinite};
#[test]
fn test_pan_zoom_boundary_combinations() {
let timeline_range = AbsoluteTimeRange::new(0, 100);
let cursor = 50;
for (target_min, target_max) in [(-50, 50), (50, 150), (-50, 150), (25, 75)] {
for (previous_start, expected_start) in [
(Infinite, Absolute(TimeInt(target_min))),
(Absolute(TimeInt(10)), Absolute(TimeInt(target_min))),
(
CursorRelative(TimeInt(-20)),
CursorRelative(TimeInt(target_min - cursor)),
),
] {
for (previous_end, expected_end) in [
(Infinite, Absolute(TimeInt(target_max))),
(Absolute(TimeInt(90)), Absolute(TimeInt(target_max))),
(
CursorRelative(TimeInt(30)),
CursorRelative(TimeInt(target_max - cursor)),
),
] {
let previous = TimeRange {
start: previous_start,
end: previous_end,
};
let recovered_range = recover_relative_boundaries_after_zoom_or_pan(
TimeInt(target_min),
TimeInt(target_max),
&previous,
TimeInt(cursor),
);
assert_eq!(
recovered_range,
TimeRange {
start: expected_start,
end: expected_end
},
"previous={previous:?}, window={target_min}..={target_max}",
);
for delta_t in [0, 25] {
let start_cursor_relative = matches!(previous_start, CursorRelative(..));
let end_cursor_relative = matches!(previous_end, CursorRelative(..));
let expected_range = AbsoluteTimeRange::new(
target_min + if start_cursor_relative { delta_t } else { 0 },
target_max + if end_cursor_relative { delta_t } else { 0 },
);
assert_eq!(
resolve_time_axis_range(
&recovered_range,
timeline_range,
TimeInt(cursor + delta_t)
),
expected_range,
"previous={previous:?}, window={target_min}..={target_max}, cursor delta={delta_t}",
);
}
}
}
}
}
#[test]
fn test_time_axis_range_after_cursor_move_keeps_window() {
let timeline_range = AbsoluteTimeRange::new(0, 10_000);
let cursor = 4_000;
for view_range in [
TimeRange::from_cursor_plus_minus(1_000),
TimeRange {
start: TimeRangeBoundary::CursorRelative(TimeInt(-100)),
end: TimeRangeBoundary::Absolute(TimeInt(5_000)),
},
] {
let window = resolve_time_axis_range(&view_range, timeline_range, TimeInt(cursor));
for new_cursor in [cursor - 2_500, cursor + 2_500] {
let shifted = time_axis_range_after_cursor_move(&view_range, cursor, new_cursor)
.expect("cursor-relative range should be shifted");
assert_eq!(
resolve_time_axis_range(&shifted, timeline_range, TimeInt(new_cursor)),
window
);
}
}
}
#[test]
fn test_time_axis_range_after_cursor_move_leaves_absolute_range_alone() {
let absolute = TimeRange {
start: TimeRangeBoundary::Absolute(TimeInt(0)),
end: TimeRangeBoundary::Absolute(TimeInt(100)),
};
assert_eq!(
time_axis_range_after_cursor_move(&absolute, 10, 20),
None,
"absolute range"
);
assert_eq!(
time_axis_range_after_cursor_move(&TimeRange::EVERYTHING, 10, 20),
None,
"infinite range"
);
}
}