1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
use std::collections::BTreeSet;
use re_chunk::{LatestAtQuery, TimeInt};
use re_entity_db::EntityDb;
use re_log_types::ResolvedTimeRange;
use crate::blueprint_timeline;
/// Max number of undo points.
///
/// TODO(emilk): decide based on how much memory the blueprint uses instead.
const MAX_UNDOS: usize = 100;
/// We store the entire edit history of a blueprint in its store.
///
/// When undoing, we move back time, and redoing move it forward.
/// When editing, we first drop all data after the current time.
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct BlueprintUndoState {
/// The current blueprint time, used for latest-at.
///
/// Everything _after_ this time is in "redo-space",
/// and will be dropped before new events are appended to the timeline.
///
/// If `None`, use the max time of the blueprint timeline.
current_time: Option<TimeInt>,
/// Interesting times to undo/redo to.
///
/// When the user drags a slider or similar, we get new events
/// recorded on each frame. The user presumably wants to undo the whole
/// slider drag, and not each increment of it.
///
/// So we use a heuristic to estimate when such interactions start/stop,
/// and add them to this set.
inflection_points: BTreeSet<TimeInt>,
}
impl BlueprintUndoState {
/// Default latest-at query
#[inline]
pub fn default_query() -> LatestAtQuery {
LatestAtQuery::latest(blueprint_timeline())
}
/// How far back in time can we undo?
pub fn oldest_undo_point(&self) -> Option<TimeInt> {
self.inflection_points.first().copied()
}
pub fn blueprint_query(&self) -> LatestAtQuery {
if let Some(time) = self.current_time {
LatestAtQuery::new(blueprint_timeline(), time)
} else {
Self::default_query()
}
}
/// If set, everything after this time is in "redo-space" (futurum).
/// If `None`, there is no undo-buffer.
pub fn redo_time(&self) -> Option<TimeInt> {
self.current_time
}
pub fn set_redo_time(&mut self, time: TimeInt) {
self.current_time = Some(time);
}
pub fn undo(&mut self, blueprint_db: &EntityDb) {
let time = self
.current_time
.unwrap_or_else(|| max_blueprint_time(blueprint_db));
if let Some(previous) = self.inflection_points.range(..time).next_back().copied() {
re_log::trace!("Undo");
self.current_time = Some(previous);
} else {
// nothing to undo to
re_log::debug!("Nothing to undo");
}
}
pub fn redo(&mut self) {
if let Some(time) = self.current_time {
re_log::trace!("Redo");
self.current_time = self.inflection_points.range(time.inc()..).next().copied();
} else {
// If we have no time, we're at latest, and have nothing to redo
re_log::debug!("Nothing to redo");
}
}
pub fn redo_all(&mut self) {
self.current_time = None;
}
/// After calling this, there is no way to redo what was once undone.
pub fn clear_redo_buffer(&mut self, blueprint_db: &mut EntityDb) {
re_tracing::profile_function!();
if let Some(last_kept_event_time) = self.current_time.take() {
let first_dropped_event_time =
TimeInt::new_temporal(last_kept_event_time.as_i64().saturating_add(1));
// Drop everything after the current timeline time
let events = blueprint_db.drop_time_range(
&blueprint_timeline(),
ResolvedTimeRange::new(first_dropped_event_time, re_chunk::TimeInt::MAX),
);
re_log::trace!("{} chunks affected when clearing redo buffer", events.len());
}
}
// Call each frame
pub fn update(&mut self, egui_ctx: &egui::Context, blueprint_db: &EntityDb) {
if is_interacting(egui_ctx) {
return;
}
// Nothing is happening - remember this as a time to undo to.
let time = max_blueprint_time(blueprint_db);
let inserted = self.inflection_points.insert(time);
if inserted {
re_log::trace!("Inserted new inflection point at {time:?}");
}
// TODO(emilk): we should _also_ look for long streaks of changes (changes every frame)
// and disregard those, in case we miss something in `is_interacting`.
// Note that this on its own won't enough though - if you drag a slider,
// then you don't want an undo-point each time you pause the mouse - only on mouse-up!
// Don't store too many undo-points:
while let Some(first) = self.inflection_points.first().copied() {
if MAX_UNDOS < self.inflection_points.len() {
self.inflection_points.remove(&first);
} else {
break;
}
}
}
}
fn max_blueprint_time(blueprint_db: &EntityDb) -> TimeInt {
blueprint_db
.time_histogram(&blueprint_timeline())
.and_then(|times| times.max_key())
.map_or(TimeInt::ZERO, TimeInt::new_temporal)
}
fn is_interacting(egui_ctx: &egui::Context) -> bool {
egui_ctx.input(|i| {
let is_scrolling = i.smooth_scroll_delta != egui::Vec2::ZERO;
let is_zooming = i.zoom_delta_2d() != egui::Vec2::splat(1.0);
i.pointer.any_down()
|| i.any_touches()
|| is_scrolling
|| !i.keys_down.is_empty()
|| is_zooming
})
}