use std::cell::RefCell;
use std::collections::HashMap;
use std::io::{BufReader, Cursor};
use ahash::HashMap as AHashMap;
use nohash_hasher::IntMap;
use re_chunk_store::LatestAtQuery;
use re_entity_db::{EntityDb, StoreBundle};
use re_log_types::{StoreId, StoreKind};
use re_ui::UiExt as _;
use crate::RERUN_TABLE_BLUEPRINT;
use re_viewer_context::{
ActiveStoreContext, ApplicationSelectionState, Contents, MissingChunkReporter, StoreCache,
SystemCommandSender as _, TimeControl, ViewClass, ViewContextSystemOncePerFrameResult, ViewId,
ViewStates, ViewSystemIdentifier, ViewerContext, VisitorControlFlow, blueprint_timeline,
};
use re_viewport::execute_systems_for_view;
use re_viewport_blueprint::{ViewBlueprint, ViewportBlueprint};
type OncePerFrameResults = IntMap<ViewSystemIdentifier, ViewContextSystemOncePerFrameResult>;
pub fn decode_table_blueprint(metadata: &HashMap<String, String>) -> Option<EntityDb> {
let encoded = metadata.get(RERUN_TABLE_BLUEPRINT)?;
let bytes = decode_blueprint_value(encoded)?;
let mut bundle = StoreBundle::from_rrd(
BufReader::new(Cursor::new(bytes)),
&re_entity_db::LogSource::EmbeddedTableBlueprint,
)
.map_err(|err| {
re_log::warn_once!("Failed to decode embedded blueprint: {err}");
err
})
.ok()?;
bundle
.drain_entity_dbs()
.find(|db| db.store_kind() == StoreKind::Blueprint)
}
#[cfg_attr(not(target_arch = "wasm32"), expect(clippy::large_enum_variant))]
pub(crate) enum PreviewRecording<'a> {
Resolved(&'a EntityDb),
Unresolved(re_uri::DatasetSegmentUri),
}
pub(crate) struct RecordingPreviewRenderer<'a> {
blueprint: &'a EntityDb,
blueprint_query: LatestAtQuery,
view_ids: Vec<ViewId>,
once_per_frame_cache: RefCell<AHashMap<StoreId, OncePerFrameResults>>,
}
impl<'a> RecordingPreviewRenderer<'a> {
pub fn from_blueprint(blueprint: &'a EntityDb) -> Option<Self> {
let blueprint_query = LatestAtQuery::latest(blueprint_timeline());
let viewport = ViewportBlueprint::from_db(blueprint, &blueprint_query);
let mut view_ids: Vec<ViewId> = Vec::new();
let _ignored = viewport.visit_contents::<()>(&mut |contents, _| {
if let Contents::View(view_id) = contents {
view_ids.push(*view_id);
}
VisitorControlFlow::Continue
});
if view_ids.is_empty() {
return None;
}
Some(Self {
blueprint,
blueprint_query,
view_ids,
once_per_frame_cache: RefCell::default(),
})
}
pub fn num_views(&self) -> usize {
self.view_ids.len()
}
pub fn show_preview(
&self,
app_ctx: &re_viewer_context::AppContext<'_>,
ui: &mut egui::Ui,
row_nr: u64,
row_hovered: bool,
recording: Option<PreviewRecording<'_>>,
view_states: &mut ViewStates,
) {
if self.view_ids.is_empty() {
return;
}
re_tracing::profile_function!();
let view_class_registry = app_ctx.view_class_registry;
let preview_state = view_states.preview_state.get_or_insert_default();
let owned_recording;
let owned_caches;
let (recording, caches) = match recording {
Some(PreviewRecording::Resolved(rec)) => {
let hub = app_ctx.storage_context;
let store_cache = hub.hub.store_caches(rec.store_id());
let Some(caches) = store_cache else {
ui.request_repaint();
return;
};
preview_state.register_recording(rec.store_id(), hub.bundle);
ui.request_repaint();
(rec, caches)
}
Some(PreviewRecording::Unresolved(uri)) => {
if let Some(err) = app_ctx.connection_registry.error_for_uri(uri) {
ui.centered_and_justified(|ui| {
ui.error_label(err);
});
return;
} else {
let recording_store_id = StoreId::new(
StoreKind::Recording,
"___preview_renderer___",
"empty_placeholder",
);
owned_recording = EntityDb::new(recording_store_id);
owned_caches = StoreCache::new(view_class_registry, &owned_recording);
(&owned_recording, &owned_caches)
}
}
None => {
let recording_store_id = StoreId::new(
StoreKind::Recording,
"___preview_renderer___",
"empty_placeholder",
);
owned_recording = EntityDb::new(recording_store_id);
owned_caches = StoreCache::new(view_class_registry, &owned_recording);
(&owned_recording, &owned_caches)
}
};
let store_context = ActiveStoreContext {
blueprint: self.blueprint,
default_blueprint: None,
recording,
caches,
should_enable_heuristics: false,
};
let visualizable_entities_per_visualizer =
caches.visualizable_entities_for_visualizer_systems();
let indicated_entities_per_visualizer = caches.indicated_entities_per_visualizer();
let store_id = recording.store_id();
let time_ctrl = preview_state
.recording_time_control(store_id)
.cloned()
.unwrap_or_else(TimeControl::preview_time_control);
let active_timeline = time_ctrl.timeline();
struct Resolved<'b> {
view_id: ViewId,
view_blueprint: ViewBlueprint,
view_class: &'b dyn ViewClass,
}
let resolved: Vec<Option<Resolved<'_>>> = self
.view_ids
.iter()
.map(|view_id| {
let view_blueprint =
ViewBlueprint::try_from_db(*view_id, self.blueprint, &self.blueprint_query)?;
let view_class =
view_class_registry.get_class_or_log_error(view_blueprint.class_identifier());
Some(Resolved {
view_id: *view_id,
view_blueprint,
view_class,
})
})
.collect();
let mut query_results = AHashMap::default();
for r in resolved.iter().flatten() {
let view_state = view_states.get_mut_or_create(store_id, r.view_id, r.view_class);
let query_range = r.view_blueprint.query_range(
self.blueprint,
&self.blueprint_query,
active_timeline,
view_class_registry,
view_state,
);
let query_result = r.view_blueprint.contents.build_data_result_tree(
&store_context,
active_timeline,
view_class_registry,
&self.blueprint_query,
&query_range,
&visualizable_entities_per_visualizer,
&indicated_entities_per_visualizer,
app_ctx.app_options,
);
query_results.insert(r.view_id, query_result);
}
let connected_receivers = Default::default();
let blueprint_time_ctrl = TimeControl::default();
let empty_selection_state = ApplicationSelectionState::default(); let ctx = ViewerContext {
app_ctx: re_viewer_context::AppContext {
active_store_context: Some(&store_context),
active_time_ctrl: Some(&time_ctrl),
selection_state: &empty_selection_state,
..app_ctx.clone()
},
connected_receivers: &connected_receivers,
store_context: &store_context,
visualizable_entities_per_visualizer: &visualizable_entities_per_visualizer,
indicated_entities_per_visualizer: &indicated_entities_per_visualizer,
query_results: &query_results,
time_ctrl: &time_ctrl,
blueprint_time_ctrl: &blueprint_time_ctrl,
blueprint_query: &self.blueprint_query,
};
let mut once_per_frame_cache = self.once_per_frame_cache.borrow_mut();
let context_system_once_per_frame_results = once_per_frame_cache
.entry(store_id.clone())
.or_insert_with(|| {
view_class_registry.run_once_per_frame_context_systems(
&ctx,
resolved
.iter()
.flatten()
.map(|r| r.view_blueprint.class_identifier()),
)
});
let mut views_rect = egui::Rect::NOTHING;
ui.spacing_mut().item_spacing.x = 0.0;
ui.columns(resolved.len(), |cols| {
for (col_ui, resolved) in cols.iter_mut().zip(resolved.into_iter()) {
let Some(Resolved {
view_id,
view_blueprint,
view_class,
}) = resolved
else {
let rect = col_ui.available_rect_before_wrap();
col_ui
.painter()
.rect_filled(rect, 2.0, col_ui.visuals().extreme_bg_color);
col_ui.allocate_rect(rect, egui::Sense::hover());
continue;
};
let view_state = view_states.get_mut_or_create(store_id, view_id, view_class);
let (view_query, system_execution_output) = execute_systems_for_view(
&ctx,
&view_blueprint,
view_state,
context_system_once_per_frame_results,
);
let missing_chunk_reporter =
MissingChunkReporter::new(system_execution_output.any_missing_chunks());
let view_state = view_states.get_mut_or_create(store_id, view_id, view_class);
let view_rect = col_ui.available_rect_before_wrap();
views_rect = views_rect.union(view_rect);
let input_before = col_ui.input_mut(|input| {
let input_before = input.clone();
input.raw.modifiers = egui::Modifiers::default();
input.raw.events.clear();
input.smooth_scroll_delta = egui::Vec2::ZERO;
input.focused = false;
input.keys_down.clear();
input.pointer = egui::PointerState::default();
input_before
});
let _result = col_ui.push_id((row_nr, view_id), |ui| {
ui.disable();
view_class.ui(
&ctx,
&missing_chunk_reporter,
ui,
view_state,
&view_query,
system_execution_output,
)
});
col_ui.input_mut(|input| {
*input = input_before;
});
re_viewport::paint_view_loading_indicator(
col_ui,
(row_nr, view_id),
view_rect,
missing_chunk_reporter.any_missing(),
recording,
);
}
});
preview_timeline(
app_ctx,
ui,
row_nr,
row_hovered,
recording,
&time_ctrl,
views_rect,
);
}
}
fn preview_timeline(
app_ctx: &re_viewer_context::AppContext<'_>,
ui: &egui::Ui,
row_nr: u64,
row_hovered: bool,
recording: &EntityDb,
time_ctrl: &TimeControl,
views_rect: egui::Rect,
) {
let id = ui.id().with(("timeline", row_nr));
let was_active = ui.read_response(id).is_some_and(|last_response| {
last_response.hovered() || last_response.dragged() || last_response.clicked()
});
if was_active || (views_rect != egui::Rect::NOTHING && row_hovered) {
let width = egui::lerp(4.0..=10.0, ui.animate_bool(id, was_active));
let timeline_rect = views_rect.with_min_y(views_rect.max.y - width);
ui.painter()
.rect_filled(timeline_rect, 0.0, ui.tokens().preview_timeline_track_color);
if let Some(time) = time_ctrl.time()
&& let Some(range) = recording.time_range_for(time_ctrl.timeline_name())
{
let response = ui.interact(
timeline_rect.expand2(egui::vec2(0.0, 4.0)),
id,
egui::Sense::click_and_drag(),
);
if (response.clicked() || response.dragged())
&& let Some(pos) = ui.input(|i| i.pointer.interact_pos())
{
let p = (pos.x - timeline_rect.min.x) / timeline_rect.width();
let p = p.clamp(0.0, 1.0);
let time = range.min.as_f64() + p as f64 * range.abs_length() as f64;
app_ctx.command_sender.send_system(
re_viewer_context::SystemCommand::TimeControlCommands {
store_id: recording.store_id().clone(),
time_commands: vec![re_viewer_context::TimeControlCommand::SetTime(
re_log_types::TimeReal::from(time),
)],
},
);
}
let p = (time.as_f64() - range.min.as_f64()) / range.abs_length() as f64;
if p > 0.0 {
ui.painter().rect_filled(
timeline_rect
.with_max_x(timeline_rect.min.x + timeline_rect.width() * p as f32),
0.0,
ui.tokens().preview_timeline_progress_color,
);
}
}
}
}
fn decode_blueprint_value(value: &str) -> Option<Vec<u8>> {
use base64::Engine as _;
let encoded = value.strip_prefix("base64:")?;
base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|err| {
re_log::warn_once!("Failed to base64-decode embedded blueprint: {err}");
err
})
.ok()
}