diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index ec5a963e8513..d06994570346 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -6,15 +6,15 @@ use re_renderer::WgpuResourcePoolStatistics; use re_smart_channel::Receiver; use re_ui::{toasts, UICommand, UICommandSender}; use re_viewer_context::{ - command_channel, AppOptions, Command, CommandReceiver, CommandSender, ComponentUiRegistry, + command_channel, AppOptions, CommandReceiver, CommandSender, ComponentUiRegistry, DynSpaceViewClass, PlayState, SpaceViewClassRegistry, SpaceViewClassRegistryError, - StoreContext, SystemCommand, + StoreContext, SystemCommand, SystemCommandSender, }; use crate::{ + app_blueprint::AppBlueprint, background_tasks::BackgroundTasks, store_hub::{StoreHub, StoreHubStats}, - ui::Blueprint, viewer_analytics::ViewerAnalytics, AppState, StoreBundle, }; @@ -245,70 +245,80 @@ impl App { } } - fn run_pending_commands( - &mut self, - blueprint: &mut Blueprint, - store_hub: &mut StoreHub, - egui_ctx: &egui::Context, - frame: &mut eframe::Frame, - ) { - while let Some(cmd) = self.command_receiver.recv() { - self.run_command(cmd, blueprint, store_hub, frame, egui_ctx); + fn run_pending_system_commands(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) { + while let Some(cmd) = self.command_receiver.recv_system() { + self.run_system_command(cmd, store_hub, egui_ctx); } } - fn run_command( + fn run_pending_ui_commands( &mut self, - cmd: Command, - blueprint: &mut Blueprint, - store_hub: &mut StoreHub, + app_blueprint: &AppBlueprint<'_>, + store_context: Option<&StoreContext<'_>>, frame: &mut eframe::Frame, - egui_ctx: &egui::Context, ) { - match cmd { - Command::SystemCommand(cmd) => self.run_system_command(cmd, store_hub), - Command::UICommand(cmd) => { - self.run_ui_command(cmd, blueprint, store_hub, frame, egui_ctx); - } + while let Some(cmd) = self.command_receiver.recv_ui() { + self.run_ui_command(cmd, app_blueprint, store_context, frame); } } #[allow(clippy::unused_self)] - fn run_system_command(&mut self, cmd: SystemCommand, store_hub: &mut StoreHub) { + fn run_system_command( + &mut self, + cmd: SystemCommand, + store_hub: &mut StoreHub, + egui_ctx: &egui::Context, + ) { match cmd { SystemCommand::SetRecordingId(recording_id) => { store_hub.set_recording_id(recording_id); } + #[cfg(not(target_arch = "wasm32"))] + SystemCommand::LoadRrd(path) => { + if let Some(rrd) = crate::loading::load_file_path(&path) { + store_hub.add_bundle(rrd); + } + } + SystemCommand::ResetViewer => self.reset(store_hub, egui_ctx), + SystemCommand::UpdateBlueprint(blueprint_id, updates) => { + let blueprint_db = store_hub.store_db_mut(&blueprint_id); + for row in updates { + match blueprint_db.entity_db.try_add_data_row(&row) { + Ok(()) => {} + Err(err) => { + re_log::warn_once!("Failed to store blueprint delta: {err}",); + } + } + } + } } } fn run_ui_command( &mut self, cmd: UICommand, - blueprint: &mut Blueprint, - store_hub: &mut StoreHub, + app_blueprint: &AppBlueprint<'_>, + store_context: Option<&StoreContext<'_>>, _frame: &mut eframe::Frame, - egui_ctx: &egui::Context, ) { - let is_narrow_screen = egui_ctx.screen_rect().width() < 600.0; // responsive ui for mobiles etc - let store_context = store_hub.read_context(); match cmd { #[cfg(not(target_arch = "wasm32"))] UICommand::Save => { - save(self, store_context.as_ref(), None); + save(self, store_context, None); } #[cfg(not(target_arch = "wasm32"))] UICommand::SaveSelection => { save( self, - store_context.as_ref(), - self.state.loop_selection(store_context.as_ref()), + store_context, + self.state.loop_selection(store_context), ); } #[cfg(not(target_arch = "wasm32"))] UICommand::Open => { - if let Some(rrd) = open_rrd_dialog() { - self.on_rrd_loaded(store_hub, rrd); + if let Some(rrd_file) = open_rrd_dialog() { + self.command_sender + .send_system(SystemCommand::LoadRrd(rrd_file)); } } #[cfg(not(target_arch = "wasm32"))] @@ -316,9 +326,7 @@ impl App { _frame.close(); } - UICommand::ResetViewer => { - self.reset(store_hub, egui_ctx); - } + UICommand::ResetViewer => self.command_sender.send_system(SystemCommand::ResetViewer), #[cfg(not(target_arch = "wasm32"))] UICommand::OpenProfiler => { @@ -329,24 +337,12 @@ impl App { self.memory_panel_open ^= true; } UICommand::ToggleBlueprintPanel => { - blueprint.blueprint_panel_expanded ^= true; - - // Only one of blueprint or selection panel can be open at a time on mobile: - if is_narrow_screen && blueprint.blueprint_panel_expanded { - blueprint.selection_panel_expanded = false; - } + app_blueprint.toggle_blueprint_panel(&self.command_sender); } UICommand::ToggleSelectionPanel => { - blueprint.selection_panel_expanded ^= true; - - // Only one of blueprint or selection panel can be open at a time on mobile: - if is_narrow_screen && blueprint.selection_panel_expanded { - blueprint.blueprint_panel_expanded = false; - } - } - UICommand::ToggleTimePanel => { - blueprint.time_panel_expanded ^= true; + app_blueprint.toggle_selection_panel(&self.command_sender); } + UICommand::ToggleTimePanel => app_blueprint.toggle_time_panel(&self.command_sender), #[cfg(not(target_arch = "wasm32"))] UICommand::ToggleFullscreen => { @@ -390,25 +386,19 @@ impl App { } UICommand::PlaybackTogglePlayPause => { - self.run_time_control_command( - store_context.as_ref(), - TimeControlCommand::TogglePlayPause, - ); + self.run_time_control_command(store_context, TimeControlCommand::TogglePlayPause); } UICommand::PlaybackFollow => { - self.run_time_control_command(store_context.as_ref(), TimeControlCommand::Follow); + self.run_time_control_command(store_context, TimeControlCommand::Follow); } UICommand::PlaybackStepBack => { - self.run_time_control_command(store_context.as_ref(), TimeControlCommand::StepBack); + self.run_time_control_command(store_context, TimeControlCommand::StepBack); } UICommand::PlaybackStepForward => { - self.run_time_control_command( - store_context.as_ref(), - TimeControlCommand::StepForward, - ); + self.run_time_control_command(store_context, TimeControlCommand::StepForward); } UICommand::PlaybackRestart => { - self.run_time_control_command(store_context.as_ref(), TimeControlCommand::Restart); + self.run_time_control_command(store_context, TimeControlCommand::Restart); } #[cfg(not(target_arch = "wasm32"))] @@ -482,7 +472,7 @@ impl App { &mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame, - blueprint: &mut Blueprint, + app_blueprint: &AppBlueprint<'_>, gpu_resource_stats: &WgpuResourcePoolStatistics, store_context: Option<&StoreContext<'_>>, store_stats: &StoreHubStats, @@ -501,7 +491,7 @@ impl App { crate::ui::mobile_warning_ui(&self.re_ui, ui); crate::ui::top_panel( - blueprint, + app_blueprint, store_context, ui, frame, @@ -518,15 +508,6 @@ impl App { // static data, but we need to jump through some hoops to // handle a missing `RecordingConfig` in this case. if let Some(store_db) = store_view.recording { - self.state - .recording_config_entry( - store_db.store_id().clone(), - self.rx.source(), - store_db, - ) - .selection_state - .on_frame_start(|item| blueprint.is_item_valid(item)); - // TODO(andreas): store the re_renderer somewhere else. let egui_renderer = { let render_state = frame.wgpu_render_state().unwrap(); @@ -539,7 +520,7 @@ impl App { render_ctx.begin_frame(); self.state.show( - blueprint, + app_blueprint, ui, render_ctx, store_db, @@ -870,16 +851,12 @@ impl eframe::App for App { let store_context = store_hub.read_context(); - let blueprint_snapshot = - Blueprint::from_db(egui_ctx, store_context.as_ref().map(|ctx| ctx.blueprint)); - - // Make a mutable copy we can edit. - let mut blueprint = blueprint_snapshot.clone(); + let app_blueprint = AppBlueprint::new(store_context.as_ref(), egui_ctx); self.ui( egui_ctx, frame, - &mut blueprint, + &app_blueprint, &gpu_resource_stats, store_context.as_ref(), &store_stats, @@ -890,8 +867,6 @@ impl eframe::App for App { paint_native_window_frame(egui_ctx); } - self.handle_dropping_files(&mut store_hub, egui_ctx); - if !self.screenshotter.is_screenshotting() { self.toasts.show(egui_ctx); } @@ -900,14 +875,11 @@ impl eframe::App for App { self.command_sender.send_ui(cmd); } - self.run_pending_commands(&mut blueprint, &mut store_hub, egui_ctx, frame); + self.run_pending_ui_commands(&app_blueprint, store_context.as_ref(), frame); - // The only way we don't have a `blueprint_id` is if we don't have a blueprint - // and the only way we don't have a blueprint is if we don't have an app. - if let Some(blueprint_id) = &blueprint.blueprint_id { - let blueprint_db = store_hub.store_db_mut(blueprint_id); - blueprint.sync_changes_to_store(&blueprint_snapshot, blueprint_db); - } + self.run_pending_system_commands(&mut store_hub, egui_ctx); + + self.handle_dropping_files(&mut store_hub, egui_ctx); // Return the `StoreHub` to the Viewer so we have it on the next frame self.store_hub = Some(store_hub); @@ -1038,15 +1010,12 @@ fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut Backg } #[cfg(not(target_arch = "wasm32"))] -fn open_rrd_dialog() -> Option { - if let Some(path) = rfd::FileDialog::new() +use std::path::PathBuf; +#[cfg(not(target_arch = "wasm32"))] +fn open_rrd_dialog() -> Option { + rfd::FileDialog::new() .add_filter("rerun data file", &["rrd"]) .pick_file() - { - crate::loading::load_file_path(&path) - } else { - None - } } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/re_viewer/src/app_blueprint.rs b/crates/re_viewer/src/app_blueprint.rs new file mode 100644 index 000000000000..c6bc767cff7c --- /dev/null +++ b/crates/re_viewer/src/app_blueprint.rs @@ -0,0 +1,119 @@ +use re_data_store::StoreDb; +use re_log_types::{DataRow, EntityPath, RowId, TimePoint}; +use re_viewer_context::{CommandSender, StoreContext, SystemCommand, SystemCommandSender}; + +use crate::blueprint_components::panel::PanelState; + +/// Blueprint for top-level application +pub struct AppBlueprint<'a> { + blueprint_db: Option<&'a StoreDb>, + is_narrow_screen: bool, + pub blueprint_panel_expanded: bool, + pub selection_panel_expanded: bool, + pub time_panel_expanded: bool, +} + +impl<'a> AppBlueprint<'a> { + pub fn new(store_ctx: Option<&'a StoreContext<'_>>, egui_ctx: &egui::Context) -> Self { + let blueprint_db = store_ctx.map(|ctx| ctx.blueprint); + let screen_size = egui_ctx.screen_rect().size(); + let mut ret = Self { + blueprint_db, + is_narrow_screen: screen_size.x < 600.0, + blueprint_panel_expanded: screen_size.x > 750.0, + selection_panel_expanded: screen_size.x > 1000.0, + time_panel_expanded: screen_size.y > 600.0, + }; + + if let Some(blueprint_db) = blueprint_db { + if let Some(expanded) = + load_panel_state(&PanelState::BLUEPRINT_VIEW_PATH.into(), blueprint_db) + { + ret.blueprint_panel_expanded = expanded; + } + if let Some(expanded) = + load_panel_state(&PanelState::SELECTION_VIEW_PATH.into(), blueprint_db) + { + ret.selection_panel_expanded = expanded; + } + if let Some(expanded) = + load_panel_state(&PanelState::TIMELINE_VIEW_PATH.into(), blueprint_db) + { + ret.time_panel_expanded = expanded; + } + } + + ret + } + + pub fn toggle_blueprint_panel(&self, command_sender: &CommandSender) { + let blueprint_panel_expanded = !self.blueprint_panel_expanded; + self.send_panel_expanded( + PanelState::BLUEPRINT_VIEW_PATH, + blueprint_panel_expanded, + command_sender, + ); + if self.is_narrow_screen && self.blueprint_panel_expanded { + self.send_panel_expanded(PanelState::SELECTION_VIEW_PATH, false, command_sender); + } + } + + pub fn toggle_selection_panel(&self, command_sender: &CommandSender) { + let selection_panel_expanded = !self.selection_panel_expanded; + self.send_panel_expanded( + PanelState::SELECTION_VIEW_PATH, + selection_panel_expanded, + command_sender, + ); + if self.is_narrow_screen && self.blueprint_panel_expanded { + self.send_panel_expanded(PanelState::BLUEPRINT_VIEW_PATH, false, command_sender); + } + } + + pub fn toggle_time_panel(&self, command_sender: &CommandSender) { + self.send_panel_expanded( + PanelState::TIMELINE_VIEW_PATH, + !self.time_panel_expanded, + command_sender, + ); + } +} + +// ---------------------------------------------------------------------------- + +impl<'a> AppBlueprint<'a> { + fn send_panel_expanded( + &self, + panel_name: &str, + expanded: bool, + command_sender: &CommandSender, + ) { + if let Some(blueprint_db) = self.blueprint_db { + let entity_path = EntityPath::from(panel_name); + // TODO(jleibs): Seq instead of timeless? + let timepoint = TimePoint::timeless(); + + let component = PanelState { expanded }; + + let row = DataRow::from_cells1_sized( + RowId::random(), + entity_path, + timepoint, + 1, + [component].as_slice(), + ); + + command_sender.send_system(SystemCommand::UpdateBlueprint( + blueprint_db.store_id().clone(), + vec![row], + )); + } + } +} + +fn load_panel_state(path: &EntityPath, blueprint_db: &re_data_store::StoreDb) -> Option { + blueprint_db + .store() + .query_timeless_component::(path) + .map(|p| p.expanded) +} diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index c1d6345406dd..972e231950be 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -7,9 +7,9 @@ use re_viewer_context::{ AppOptions, Caches, CommandSender, ComponentUiRegistry, PlayState, RecordingConfig, SpaceViewClassRegistry, StoreContext, ViewerContext, }; -use re_viewport::ViewportState; +use re_viewport::{SpaceInfoCollection, ViewportBlueprint, ViewportState}; -use crate::{store_hub::StoreHub, ui::Blueprint}; +use crate::{app_blueprint::AppBlueprint, store_hub::StoreHub, ui::blueprint_panel_ui}; const WATERMARK: bool = false; // Nice for recording media material @@ -70,7 +70,7 @@ impl AppState { #[allow(clippy::too_many_arguments)] pub fn show( &mut self, - blueprint: &mut Blueprint, + app_blueprint: &AppBlueprint<'_>, ui: &mut egui::Ui, render_ctx: &mut re_renderer::RenderContext, store_db: &StoreDb, @@ -83,6 +83,8 @@ impl AppState { ) { re_tracing::profile_function!(); + let mut blueprint = ViewportBlueprint::from_db(store_context.blueprint); + let Self { app_options, cache, @@ -92,6 +94,15 @@ impl AppState { viewport_state, } = self; + recording_config_entry( + recording_configs, + store_db.store_id().clone(), + rx.source(), + store_db, + ) + .selection_state + .on_frame_start(|item| blueprint.is_item_valid(item)); + let rec_cfg = recording_config_entry( recording_configs, store_db.store_id().clone(), @@ -112,8 +123,14 @@ impl AppState { command_sender, }; - time_panel.show_panel(&mut ctx, ui, blueprint.time_panel_expanded); - selection_panel.show_panel(viewport_state, &mut ctx, ui, blueprint); + time_panel.show_panel(&mut ctx, ui, app_blueprint.time_panel_expanded); + selection_panel.show_panel( + viewport_state, + &mut ctx, + ui, + &mut blueprint, + app_blueprint.selection_panel_expanded, + ); let central_panel_frame = egui::Frame { fill: ui.style().visuals.panel_fill, @@ -124,9 +141,39 @@ impl AppState { egui::CentralPanel::default() .frame(central_panel_frame) .show_inside(ui, |ui| { - blueprint.blueprint_panel_and_viewport(viewport_state, &mut ctx, ui); + re_tracing::profile_function!(); + + let spaces_info = SpaceInfoCollection::new(&ctx.store_db.entity_db); + + blueprint.viewport.on_frame_start(&mut ctx, &spaces_info); + + blueprint_panel_ui( + &mut blueprint, + &mut ctx, + ui, + &spaces_info, + app_blueprint.blueprint_panel_expanded, + ); + + let viewport_frame = egui::Frame { + fill: ui.style().visuals.panel_fill, + ..Default::default() + }; + + egui::CentralPanel::default() + .frame(viewport_frame) + .show_inside(ui, |ui| { + blueprint.viewport.viewport_ui(viewport_state, ui, &mut ctx); + }); + + // If the viewport was user-edited, then disable auto space views + if blueprint.viewport.has_been_user_edited { + blueprint.viewport.auto_space_views = false; + } }); + blueprint.sync_changes(command_sender); + { // We move the time at the very end of the frame, // so we have one frame to see the first data before we move the time. @@ -151,15 +198,6 @@ impl AppState { self.recording_configs.get_mut(rec_id) } - pub fn recording_config_entry( - &mut self, - id: StoreId, - data_source: &'_ re_smart_channel::SmartChannelSource, - store_db: &'_ StoreDb, - ) -> &mut RecordingConfig { - recording_config_entry(&mut self.recording_configs, id, data_source, store_db) - } - pub fn cleanup(&mut self, store_hub: &StoreHub) { re_tracing::profile_function!(); diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index 01aa95268926..99c2a8404c3b 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -4,6 +4,7 @@ //! including all 2D and 3D visualization code. mod app; +mod app_blueprint; mod app_state; mod background_tasks; pub mod blueprint_components; diff --git a/crates/re_viewer/src/ui/blueprint.rs b/crates/re_viewer/src/ui/blueprint.rs deleted file mode 100644 index d0f4b6d09918..000000000000 --- a/crates/re_viewer/src/ui/blueprint.rs +++ /dev/null @@ -1,158 +0,0 @@ -use re_log_types::StoreId; -use re_viewer_context::{Item, ViewerContext}; -use re_viewport::{SpaceInfoCollection, Viewport, ViewportState}; - -/// Defines the layout of the whole Viewer (or will, eventually). -#[derive(Clone)] -pub struct Blueprint { - pub blueprint_id: Option, - - pub blueprint_panel_expanded: bool, - pub selection_panel_expanded: bool, - pub time_panel_expanded: bool, - - pub viewport: Viewport, -} - -impl Blueprint { - /// Create a [`Blueprint`] with appropriate defaults. - pub fn new(blueprint_id: Option, egui_ctx: &egui::Context) -> Self { - let screen_size = egui_ctx.screen_rect().size(); - Self { - blueprint_id, - blueprint_panel_expanded: screen_size.x > 750.0, - selection_panel_expanded: screen_size.x > 1000.0, - time_panel_expanded: screen_size.y > 600.0, - viewport: Default::default(), - } - } - - pub fn blueprint_panel_and_viewport( - &mut self, - viewport_state: &mut ViewportState, - ctx: &mut ViewerContext<'_>, - ui: &mut egui::Ui, - ) { - re_tracing::profile_function!(); - - let spaces_info = SpaceInfoCollection::new(&ctx.store_db.entity_db); - - self.viewport.on_frame_start(ctx, &spaces_info); - - self.blueprint_panel(ctx, ui, &spaces_info); - - let viewport_frame = egui::Frame { - fill: ui.style().visuals.panel_fill, - ..Default::default() - }; - - egui::CentralPanel::default() - .frame(viewport_frame) - .show_inside(ui, |ui| { - self.viewport.viewport_ui(viewport_state, ui, ctx); - }); - - // If the viewport was user-edited, then disable auto space views - if self.viewport.has_been_user_edited { - self.viewport.auto_space_views = false; - } - } - - fn blueprint_panel( - &mut self, - ctx: &mut ViewerContext<'_>, - ui: &mut egui::Ui, - spaces_info: &SpaceInfoCollection, - ) { - let screen_width = ui.ctx().screen_rect().width(); - - let panel = egui::SidePanel::left("blueprint_panel") - .resizable(true) - .frame(egui::Frame { - fill: ui.visuals().panel_fill, - ..Default::default() - }) - .min_width(120.0) - .default_width((0.35 * screen_width).min(200.0).round()); - - panel.show_animated_inside(ui, self.blueprint_panel_expanded, |ui: &mut egui::Ui| { - self.title_bar_ui(ctx, ui, spaces_info); - - egui::Frame { - inner_margin: egui::Margin::same(re_ui::ReUi::view_padding()), - ..Default::default() - } - .show(ui, |ui| { - self.viewport.tree_ui(ctx, ui); - }); - }); - } - - fn title_bar_ui( - &mut self, - ctx: &mut ViewerContext<'_>, - ui: &mut egui::Ui, - spaces_info: &SpaceInfoCollection, - ) { - egui::TopBottomPanel::top("blueprint_panel_title_bar") - .exact_height(re_ui::ReUi::title_bar_height()) - .frame(egui::Frame { - inner_margin: egui::Margin::symmetric(re_ui::ReUi::view_padding(), 0.0), - ..Default::default() - }) - .show_inside(ui, |ui| { - ui.horizontal_centered(|ui| { - ui.strong("Blueprint").on_hover_text( - "The Blueprint is where you can configure the Rerun Viewer.", - ); - - ui.allocate_ui_with_layout( - ui.available_size_before_wrap(), - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - self.viewport - .add_new_spaceview_button_ui(ctx, ui, spaces_info); - self.reset_button_ui(ctx, ui, spaces_info); - }, - ); - }); - }); - } - - fn reset_button_ui( - &mut self, - ctx: &mut ViewerContext<'_>, - ui: &mut egui::Ui, - spaces_info: &SpaceInfoCollection, - ) { - if ctx - .re_ui - .small_icon_button(ui, &re_ui::icons::RESET) - .on_hover_text("Re-populate Viewport with automatically chosen Space Views") - .clicked() - { - self.viewport = Viewport::new(ctx, spaces_info); - } - } - - /// If `false`, the item is referring to data that is not present in this blueprint. - pub fn is_item_valid(&self, item: &Item) -> bool { - match item { - Item::ComponentPath(_) => true, - Item::InstancePath(space_view_id, _) => space_view_id - .map(|space_view_id| self.viewport.space_view(&space_view_id).is_some()) - .unwrap_or(true), - Item::SpaceView(space_view_id) => self.viewport.space_view(space_view_id).is_some(), - Item::DataBlueprintGroup(space_view_id, data_blueprint_group_handle) => { - if let Some(space_view) = self.viewport.space_view(space_view_id) { - space_view - .data_blueprint - .group(*data_blueprint_group_handle) - .is_some() - } else { - false - } - } - } - } -} diff --git a/crates/re_viewer/src/ui/blueprint_load.rs b/crates/re_viewer/src/ui/blueprint_load.rs deleted file mode 100644 index b63274e6a637..000000000000 --- a/crates/re_viewer/src/ui/blueprint_load.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::collections::BTreeMap; - -use ahash::HashMap; - -use re_data_store::EntityPath; -use re_viewer_context::SpaceViewId; -use re_viewport::{ - blueprint_components::{ - AutoSpaceViews, SpaceViewComponent, SpaceViewMaximized, SpaceViewVisibility, - ViewportLayout, VIEWPORT_PATH, - }, - SpaceViewBlueprint, Viewport, -}; - -use super::Blueprint; -use crate::blueprint_components::panel::PanelState; - -impl Blueprint { - pub fn from_db( - egui_ctx: &egui::Context, - blueprint_db: Option<&re_data_store::StoreDb>, - ) -> Self { - let mut ret = Self::new(blueprint_db.map(|bp| bp.store_id()).cloned(), egui_ctx); - - if let Some(blueprint_db) = blueprint_db { - let space_views: HashMap = if let Some(space_views) = - blueprint_db - .entity_db - .tree - .children - .get(&re_data_store::EntityPathPart::Name( - SpaceViewComponent::SPACEVIEW_PREFIX.into(), - )) { - space_views - .children - .values() - .filter_map(|view_tree| load_space_view(&view_tree.path, blueprint_db)) - .map(|sv| (sv.id, sv)) - .collect() - } else { - Default::default() - }; - - ret.viewport = load_viewport(blueprint_db, space_views); - - if let Some(expanded) = - load_panel_state(&PanelState::BLUEPRINT_VIEW_PATH.into(), blueprint_db) - { - ret.blueprint_panel_expanded = expanded; - } - if let Some(expanded) = - load_panel_state(&PanelState::SELECTION_VIEW_PATH.into(), blueprint_db) - { - ret.selection_panel_expanded = expanded; - } - if let Some(expanded) = - load_panel_state(&PanelState::TIMELINE_VIEW_PATH.into(), blueprint_db) - { - ret.time_panel_expanded = expanded; - } - } - ret - } -} - -fn load_panel_state(path: &EntityPath, blueprint_db: &re_data_store::StoreDb) -> Option { - blueprint_db - .store() - .query_timeless_component::(path) - .map(|p| p.expanded) -} - -fn load_space_view( - path: &EntityPath, - blueprint_db: &re_data_store::StoreDb, -) -> Option { - blueprint_db - .store() - .query_timeless_component::(path) - .map(|c| c.space_view) -} - -fn load_viewport( - blueprint_db: &re_data_store::StoreDb, - space_views: HashMap, -) -> Viewport { - let auto_space_views = blueprint_db - .store() - .query_timeless_component::(&VIEWPORT_PATH.into()) - .unwrap_or_else(|| { - // Only enable auto-space-views if this is the app-default blueprint - AutoSpaceViews( - blueprint_db - .store_info() - .map_or(false, |ri| ri.is_app_default_blueprint()), - ) - }); - - let space_view_visibility = blueprint_db - .store() - .query_timeless_component::(&VIEWPORT_PATH.into()) - .unwrap_or_default(); - - let space_view_maximized = blueprint_db - .store() - .query_timeless_component::(&VIEWPORT_PATH.into()) - .unwrap_or_default(); - - let viewport_layout: ViewportLayout = blueprint_db - .store() - .query_timeless_component::(&VIEWPORT_PATH.into()) - .unwrap_or_default(); - - let unknown_space_views: HashMap<_, _> = space_views - .iter() - .filter(|(k, _)| !viewport_layout.space_view_keys.contains(k)) - .map(|(k, v)| (*k, v.clone())) - .collect(); - - let known_space_views: BTreeMap<_, _> = space_views - .into_iter() - .filter(|(k, _)| viewport_layout.space_view_keys.contains(k)) - .collect(); - - let mut viewport = Viewport { - space_views: known_space_views, - visible: space_view_visibility.0, - trees: viewport_layout.trees, - maximized: space_view_maximized.0, - has_been_user_edited: viewport_layout.has_been_user_edited, - auto_space_views: auto_space_views.0, - }; - // TODO(jleibs): It seems we shouldn't call this until later, after we've created - // the snapshot. Doing this here means we are mutating the state before it goes - // into the snapshot. For example, even if there's no visibility in the - // store, this will end up with default-visibility, which then *won't* be saved back. - for (_, view) in unknown_space_views { - viewport.add_space_view(view); - } - - viewport -} diff --git a/crates/re_viewer/src/ui/blueprint_panel.rs b/crates/re_viewer/src/ui/blueprint_panel.rs new file mode 100644 index 000000000000..0b990fd9e948 --- /dev/null +++ b/crates/re_viewer/src/ui/blueprint_panel.rs @@ -0,0 +1,81 @@ +use re_viewer_context::ViewerContext; +use re_viewport::{SpaceInfoCollection, Viewport, ViewportBlueprint}; + +/// Show the left-handle panel based on the current [`ViewportBlueprint`] +pub fn blueprint_panel_ui( + blueprint: &mut ViewportBlueprint<'_>, + ctx: &mut ViewerContext<'_>, + ui: &mut egui::Ui, + spaces_info: &SpaceInfoCollection, + expanded: bool, +) { + let screen_width = ui.ctx().screen_rect().width(); + + let panel = egui::SidePanel::left("blueprint_panel") + .resizable(true) + .frame(egui::Frame { + fill: ui.visuals().panel_fill, + ..Default::default() + }) + .min_width(120.0) + .default_width((0.35 * screen_width).min(200.0).round()); + + panel.show_animated_inside(ui, expanded, |ui: &mut egui::Ui| { + title_bar_ui(blueprint, ctx, ui, spaces_info); + + egui::Frame { + inner_margin: egui::Margin::same(re_ui::ReUi::view_padding()), + ..Default::default() + } + .show(ui, |ui| { + blueprint.viewport.tree_ui(ctx, ui); + }); + }); +} + +fn title_bar_ui( + blueprint: &mut ViewportBlueprint<'_>, + ctx: &mut ViewerContext<'_>, + ui: &mut egui::Ui, + spaces_info: &SpaceInfoCollection, +) { + egui::TopBottomPanel::top("blueprint_panel_title_bar") + .exact_height(re_ui::ReUi::title_bar_height()) + .frame(egui::Frame { + inner_margin: egui::Margin::symmetric(re_ui::ReUi::view_padding(), 0.0), + ..Default::default() + }) + .show_inside(ui, |ui| { + ui.horizontal_centered(|ui| { + ui.strong("Blueprint") + .on_hover_text("The Blueprint is where you can configure the Rerun Viewer."); + + ui.allocate_ui_with_layout( + ui.available_size_before_wrap(), + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + blueprint + .viewport + .add_new_spaceview_button_ui(ctx, ui, spaces_info); + reset_button_ui(blueprint, ctx, ui, spaces_info); + }, + ); + }); + }); +} + +fn reset_button_ui( + blueprint: &mut ViewportBlueprint<'_>, + ctx: &mut ViewerContext<'_>, + ui: &mut egui::Ui, + spaces_info: &SpaceInfoCollection, +) { + if ctx + .re_ui + .small_icon_button(ui, &re_ui::icons::RESET) + .on_hover_text("Re-populate Viewport with automatically chosen Space Views") + .clicked() + { + blueprint.viewport = Viewport::new(ctx, spaces_info); + } +} diff --git a/crates/re_viewer/src/ui/blueprint_sync.rs b/crates/re_viewer/src/ui/blueprint_sync.rs deleted file mode 100644 index d2a9edb8d116..000000000000 --- a/crates/re_viewer/src/ui/blueprint_sync.rs +++ /dev/null @@ -1,177 +0,0 @@ -use arrow2_convert::field::ArrowField; -use re_data_store::store_one_component; -use re_log_types::{Component, DataCell, DataRow, EntityPath, RowId, TimePoint}; -use re_viewer_context::SpaceViewId; -use re_viewport::{ - blueprint_components::{ - AutoSpaceViews, SpaceViewComponent, SpaceViewMaximized, SpaceViewVisibility, - ViewportLayout, VIEWPORT_PATH, - }, - SpaceViewBlueprint, Viewport, -}; - -use super::Blueprint; -use crate::blueprint_components::panel::PanelState; - -// Resolving and applying updates -impl Blueprint { - pub fn sync_changes_to_store( - &self, - snapshot: &Self, - blueprint_db: &mut re_data_store::StoreDb, - ) { - // Update the panel states - sync_panel_expanded( - blueprint_db, - PanelState::BLUEPRINT_VIEW_PATH, - self.blueprint_panel_expanded, - snapshot.blueprint_panel_expanded, - ); - sync_panel_expanded( - blueprint_db, - PanelState::SELECTION_VIEW_PATH, - self.selection_panel_expanded, - snapshot.selection_panel_expanded, - ); - sync_panel_expanded( - blueprint_db, - PanelState::TIMELINE_VIEW_PATH, - self.time_panel_expanded, - snapshot.time_panel_expanded, - ); - - sync_viewport(blueprint_db, &self.viewport, &snapshot.viewport); - - // Add any new or modified space views - for id in self.viewport.space_view_ids() { - if let Some(space_view) = self.viewport.space_view(id) { - sync_space_view(blueprint_db, space_view, snapshot.viewport.space_view(id)); - } - } - - // Remove any deleted space views - for space_view_id in snapshot.viewport.space_view_ids() { - if self.viewport.space_view(space_view_id).is_none() { - clear_space_view(blueprint_db, space_view_id); - } - } - } -} - -pub fn sync_panel_expanded( - blueprint_db: &mut re_data_store::StoreDb, - panel_name: &str, - expanded: bool, - snapshot: bool, -) { - if expanded != snapshot { - let entity_path = EntityPath::from(panel_name); - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - - let component = PanelState { expanded }; - - store_one_component(blueprint_db, &entity_path, &timepoint, component); - } -} - -pub fn sync_space_view( - blueprint_db: &mut re_data_store::StoreDb, - space_view: &SpaceViewBlueprint, - snapshot: Option<&SpaceViewBlueprint>, -) { - if Some(space_view) != snapshot { - let entity_path = EntityPath::from(format!( - "{}/{}", - SpaceViewComponent::SPACEVIEW_PREFIX, - space_view.id - )); - - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - - let component = SpaceViewComponent { - space_view: space_view.clone(), - }; - - store_one_component(blueprint_db, &entity_path, &timepoint, component); - } -} - -pub fn clear_space_view(blueprint_db: &mut re_data_store::StoreDb, space_view_id: &SpaceViewId) { - let entity_path = EntityPath::from(format!( - "{}/{}", - SpaceViewComponent::SPACEVIEW_PREFIX, - space_view_id - )); - - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - - let cell = - DataCell::from_arrow_empty(SpaceViewComponent::name(), SpaceViewComponent::data_type()); - - let mut row = DataRow::from_cells1(RowId::random(), entity_path, timepoint, 0, cell); - row.compute_all_size_bytes(); - - match blueprint_db.entity_db.try_add_data_row(&row) { - Ok(()) => {} - Err(err) => { - re_log::warn_once!("Failed to clear space view {}: {err}", space_view_id,); - } - } -} - -pub fn sync_viewport( - blueprint_db: &mut re_data_store::StoreDb, - viewport: &Viewport, - snapshot: &Viewport, -) { - let entity_path = EntityPath::from(VIEWPORT_PATH); - - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - - if viewport.auto_space_views != snapshot.auto_space_views { - let component = AutoSpaceViews(viewport.auto_space_views); - store_one_component(blueprint_db, &entity_path, &timepoint, component); - } - - if viewport.visible != snapshot.visible { - let component = SpaceViewVisibility(viewport.visible.clone()); - store_one_component(blueprint_db, &entity_path, &timepoint, component); - } - - if viewport.maximized != snapshot.maximized { - let component = SpaceViewMaximized(viewport.maximized); - store_one_component(blueprint_db, &entity_path, &timepoint, component); - } - - // Note: we can't just check `viewport.trees != snapshot.trees` because the - // tree contains serde[skip]'d state that won't match in PartialEq. - if viewport.trees.len() != snapshot.trees.len() - || !viewport.trees.iter().zip(snapshot.trees.iter()).all( - |((left_vis, left_tree), (right_vis, right_tree))| { - left_vis == right_vis - && left_tree.root == right_tree.root - && left_tree.tiles.tiles == right_tree.tiles.tiles - }, - ) - || viewport.has_been_user_edited != snapshot.has_been_user_edited - { - let component = ViewportLayout { - space_view_keys: viewport.space_views.keys().cloned().collect(), - trees: viewport.trees.clone(), - has_been_user_edited: viewport.has_been_user_edited, - }; - - store_one_component(blueprint_db, &entity_path, &timepoint, component); - - // TODO(jleibs): Sort out this causality mess - // If we are saving a new layout, we also need to save the visibility-set because - // it gets mutated on load but isn't guaranteed to be mutated on layout-change - // which means it won't get saved. - let component = SpaceViewVisibility(viewport.visible.clone()); - store_one_component(blueprint_db, &entity_path, &timepoint, component); - } -} diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs index 2e5a491a9a6f..b8fc1d42add2 100644 --- a/crates/re_viewer/src/ui/mod.rs +++ b/crates/re_viewer/src/ui/mod.rs @@ -1,6 +1,4 @@ -mod blueprint; -mod blueprint_load; -mod blueprint_sync; +mod blueprint_panel; mod mobile_warning_ui; mod rerun_menu; mod selection_history_ui; @@ -10,10 +8,10 @@ mod wait_screen_ui; pub(crate) mod memory_panel; pub(crate) mod selection_panel; +pub use blueprint_panel::blueprint_panel_ui; // ---- pub(crate) use { - self::blueprint::Blueprint, self::mobile_warning_ui::mobile_warning_ui, - self::rerun_menu::rerun_menu_button_ui, self::top_panel::top_panel, - self::wait_screen_ui::wait_screen_ui, + self::mobile_warning_ui::mobile_warning_ui, self::rerun_menu::rerun_menu_button_ui, + self::top_panel::top_panel, self::wait_screen_ui::wait_screen_ui, }; diff --git a/crates/re_viewer/src/ui/selection_history_ui.rs b/crates/re_viewer/src/ui/selection_history_ui.rs index d8e2afe87254..1f77b729ba3b 100644 --- a/crates/re_viewer/src/ui/selection_history_ui.rs +++ b/crates/re_viewer/src/ui/selection_history_ui.rs @@ -1,8 +1,7 @@ use egui::RichText; use re_ui::UICommand; use re_viewer_context::{Item, ItemCollection, SelectionHistory}; - -use crate::ui::Blueprint; +use re_viewport::ViewportBlueprint; // --- @@ -15,7 +14,7 @@ impl SelectionHistoryUi { &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, - blueprint: &Blueprint, + blueprint: &ViewportBlueprint<'_>, history: &mut SelectionHistory, ) -> Option { ui.horizontal_centered(|ui| { @@ -37,7 +36,7 @@ impl SelectionHistoryUi { &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, - blueprint: &Blueprint, + blueprint: &ViewportBlueprint<'_>, history: &mut SelectionHistory, ) -> Option { // undo selection @@ -81,7 +80,7 @@ impl SelectionHistoryUi { &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, - blueprint: &Blueprint, + blueprint: &ViewportBlueprint<'_>, history: &mut SelectionHistory, ) -> Option { // redo selection @@ -124,7 +123,7 @@ impl SelectionHistoryUi { #[allow(clippy::unused_self)] fn history_item_ui( &mut self, - blueprint: &Blueprint, + blueprint: &ViewportBlueprint<'_>, ui: &mut egui::Ui, index: usize, history: &mut SelectionHistory, @@ -155,7 +154,7 @@ fn item_kind_ui(ui: &mut egui::Ui, sel: &Item) { ui.weak(RichText::new(format!("({})", sel.kind()))); } -fn item_collection_to_string(blueprint: &Blueprint, items: &ItemCollection) -> String { +fn item_collection_to_string(blueprint: &ViewportBlueprint<'_>, items: &ItemCollection) -> String { assert!(!items.is_empty()); // history never contains empty selections. if items.len() == 1 { item_to_string(blueprint, items.iter().next().unwrap()) @@ -166,7 +165,7 @@ fn item_collection_to_string(blueprint: &Blueprint, items: &ItemCollection) -> S } } -fn item_to_string(blueprint: &Blueprint, item: &Item) -> String { +fn item_to_string(blueprint: &ViewportBlueprint<'_>, item: &Item) -> String { match item { Item::SpaceView(sid) => { if let Some(space_view) = blueprint.viewport.space_view(sid) { diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_viewer/src/ui/selection_panel.rs index a3116373110d..cbf5e559de39 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_viewer/src/ui/selection_panel.rs @@ -5,9 +5,7 @@ use re_data_store::{ColorMapper, Colormap, EditableAutoValue, EntityPath, Entity use re_data_ui::{item_ui, DataUi}; use re_log_types::TimeType; use re_viewer_context::{Item, SpaceViewId, UiVerbosity, ViewerContext}; -use re_viewport::{Viewport, ViewportState}; - -use crate::ui::Blueprint; +use re_viewport::{Viewport, ViewportBlueprint, ViewportState}; use super::selection_history_ui::SelectionHistoryUi; @@ -26,7 +24,8 @@ impl SelectionPanel { viewport_state: &mut ViewportState, ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui, - blueprint: &mut Blueprint, + blueprint: &mut ViewportBlueprint<'_>, + expanded: bool, ) { let screen_width = ui.ctx().screen_rect().width(); @@ -40,41 +39,37 @@ impl SelectionPanel { ..Default::default() }); - panel.show_animated_inside( - ui, - blueprint.selection_panel_expanded, - |ui: &mut egui::Ui| { - egui::TopBottomPanel::top("selection_panel_title_bar") - .exact_height(re_ui::ReUi::title_bar_height()) - .frame(egui::Frame { - inner_margin: egui::Margin::symmetric(re_ui::ReUi::view_padding(), 0.0), - ..Default::default() - }) - .show_inside(ui, |ui| { - if let Some(selection) = self.selection_state_ui.selection_ui( - ctx.re_ui, - ui, - blueprint, - &mut ctx.selection_state_mut().history, - ) { - ctx.selection_state_mut() - .set_selection(selection.iter().cloned()); - } - }); + panel.show_animated_inside(ui, expanded, |ui: &mut egui::Ui| { + egui::TopBottomPanel::top("selection_panel_title_bar") + .exact_height(re_ui::ReUi::title_bar_height()) + .frame(egui::Frame { + inner_margin: egui::Margin::symmetric(re_ui::ReUi::view_padding(), 0.0), + ..Default::default() + }) + .show_inside(ui, |ui| { + if let Some(selection) = self.selection_state_ui.selection_ui( + ctx.re_ui, + ui, + blueprint, + &mut ctx.selection_state_mut().history, + ) { + ctx.selection_state_mut() + .set_selection(selection.iter().cloned()); + } + }); - egui::ScrollArea::both() - .auto_shrink([false; 2]) + egui::ScrollArea::both() + .auto_shrink([false; 2]) + .show(ui, |ui| { + egui::Frame { + inner_margin: egui::Margin::same(re_ui::ReUi::view_padding()), + ..Default::default() + } .show(ui, |ui| { - egui::Frame { - inner_margin: egui::Margin::same(re_ui::ReUi::view_padding()), - ..Default::default() - } - .show(ui, |ui| { - self.contents(viewport_state, ctx, ui, blueprint); - }); + self.contents(viewport_state, ctx, ui, blueprint); }); - }, - ); + }); + }); } #[allow(clippy::unused_self)] @@ -83,7 +78,7 @@ impl SelectionPanel { viewport_state: &mut ViewportState, ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui, - blueprint: &mut Blueprint, + blueprint: &mut ViewportBlueprint<'_>, ) { re_tracing::profile_function!(); @@ -213,7 +208,7 @@ fn blueprint_ui( viewport_state: &mut ViewportState, ui: &mut egui::Ui, ctx: &mut ViewerContext<'_>, - blueprint: &mut Blueprint, + blueprint: &mut ViewportBlueprint<'_>, item: &Item, ) { match item { @@ -311,7 +306,7 @@ fn list_existing_data_blueprints( ui: &mut egui::Ui, ctx: &mut ViewerContext<'_>, entity_path: &EntityPath, - blueprint: &Blueprint, + blueprint: &ViewportBlueprint<'_>, ) { let space_views_with_path = blueprint .viewport diff --git a/crates/re_viewer/src/ui/top_panel.rs b/crates/re_viewer/src/ui/top_panel.rs index 1bcbafb82f41..d69e08e59722 100644 --- a/crates/re_viewer/src/ui/top_panel.rs +++ b/crates/re_viewer/src/ui/top_panel.rs @@ -1,12 +1,12 @@ use re_format::format_number; use re_renderer::WgpuResourcePoolStatistics; -use re_ui::{UICommand, UICommandSender}; +use re_ui::UICommand; use re_viewer_context::StoreContext; -use crate::{ui::Blueprint, App}; +use crate::{app_blueprint::AppBlueprint, App}; pub fn top_panel( - blueprint: &Blueprint, + app_blueprint: &AppBlueprint<'_>, store_context: Option<&StoreContext<'_>>, ui: &mut egui::Ui, frame: &mut eframe::Frame, @@ -39,7 +39,14 @@ pub fn top_panel( ui.set_height(top_bar_style.height); ui.add_space(top_bar_style.indent); - top_bar_ui(blueprint, store_context, ui, frame, app, gpu_resource_stats); + top_bar_ui( + app_blueprint, + store_context, + ui, + frame, + app, + gpu_resource_stats, + ); }) .response; @@ -56,7 +63,7 @@ pub fn top_panel( } fn top_bar_ui( - blueprint: &Blueprint, + app_blueprint: &AppBlueprint<'_>, store_context: Option<&StoreContext<'_>>, ui: &mut egui::Ui, frame: &mut eframe::Frame, @@ -85,7 +92,7 @@ fn top_bar_ui( ui.add_space(extra_margin); } - let mut selection_panel_expanded = blueprint.selection_panel_expanded; + let mut selection_panel_expanded = app_blueprint.selection_panel_expanded; if app .re_ui() .medium_icon_toggle_button( @@ -99,10 +106,10 @@ fn top_bar_ui( )) .clicked() { - app.command_sender.send_ui(UICommand::ToggleSelectionPanel); + app_blueprint.toggle_selection_panel(&app.command_sender); } - let mut time_panel_expanded = blueprint.time_panel_expanded; + let mut time_panel_expanded = app_blueprint.time_panel_expanded; if app .re_ui() .medium_icon_toggle_button( @@ -116,10 +123,10 @@ fn top_bar_ui( )) .clicked() { - app.command_sender.send_ui(UICommand::ToggleTimePanel); + app_blueprint.toggle_time_panel(&app.command_sender); } - let mut blueprint_panel_expanded = blueprint.blueprint_panel_expanded; + let mut blueprint_panel_expanded = app_blueprint.blueprint_panel_expanded; if app .re_ui() .medium_icon_toggle_button( @@ -133,7 +140,7 @@ fn top_bar_ui( )) .clicked() { - app.command_sender.send_ui(UICommand::ToggleBlueprintPanel); + app_blueprint.toggle_blueprint_panel(&app.command_sender); } if cfg!(debug_assertions) && app.app_options().show_metrics { diff --git a/crates/re_viewer_context/src/command_sender.rs b/crates/re_viewer_context/src/command_sender.rs index 991fe7673464..6abbdcd916c9 100644 --- a/crates/re_viewer_context/src/command_sender.rs +++ b/crates/re_viewer_context/src/command_sender.rs @@ -1,4 +1,4 @@ -use re_log_types::StoreId; +use re_log_types::{DataRow, StoreId}; use re_ui::{UICommand, UICommandSender}; // ---------------------------------------------------------------------------- @@ -6,7 +6,22 @@ use re_ui::{UICommand, UICommandSender}; /// Commands used by internal system components // TODO(jleibs): Is there a better crate for this? pub enum SystemCommand { + /// Load an RRD by Filename + #[cfg(not(target_arch = "wasm32"))] + LoadRrd(std::path::PathBuf), + + /// Reset the `Viewer` to the default state + ResetViewer, + + /// Change the active recording-id in the `StoreHub` SetRecordingId(StoreId), + + /// Update the blueprint with additional data + /// + /// The [`StoreId`] should generally be the currently selected blueprint + /// but is tracked manually to ensure self-consistency if the blueprint + /// is both modified and changed in the same frame. + UpdateBlueprint(StoreId, Vec), } /// Interface for sending [`SystemCommand`] messages. @@ -16,31 +31,48 @@ pub trait SystemCommandSender { // ---------------------------------------------------------------------------- -/// Generic Command -pub enum Command { - SystemCommand(SystemCommand), - UICommand(UICommand), +/// Sender that queues up the execution of commands. +pub struct CommandSender { + system_sender: std::sync::mpsc::Sender, + ui_sender: std::sync::mpsc::Sender, } -/// Sender that queues up the execution of a command. -pub struct CommandSender(std::sync::mpsc::Sender); - /// Receiver for the [`CommandSender`] -pub struct CommandReceiver(std::sync::mpsc::Receiver); +pub struct CommandReceiver { + system_receiver: std::sync::mpsc::Receiver, + ui_receiver: std::sync::mpsc::Receiver, +} impl CommandReceiver { - /// Receive a command to be executed if any is queued. - pub fn recv(&self) -> Option { + /// Receive a [`SystemCommand`] to be executed if any is queued. + pub fn recv_system(&self) -> Option { + // The only way this can fail (other than being empty) + // is if the sender has been dropped. + self.system_receiver.try_recv().ok() + } + + /// Receive a [`UICommand`] to be executed if any is queued. + pub fn recv_ui(&self) -> Option { // The only way this can fail (other than being empty) // is if the sender has been dropped. - self.0.try_recv().ok() + self.ui_receiver.try_recv().ok() } } /// Creates a new command channel. pub fn command_channel() -> (CommandSender, CommandReceiver) { - let (sender, receiver) = std::sync::mpsc::channel(); - (CommandSender(sender), CommandReceiver(receiver)) + let (system_sender, system_receiver) = std::sync::mpsc::channel(); + let (ui_sender, ui_receiver) = std::sync::mpsc::channel(); + ( + CommandSender { + system_sender, + ui_sender, + }, + CommandReceiver { + system_receiver, + ui_receiver, + }, + ) } // ---------------------------------------------------------------------------- @@ -49,7 +81,7 @@ impl SystemCommandSender for CommandSender { /// Send a command to be executed. fn send_system(&self, command: SystemCommand) { // The only way this can fail is if the receiver has been dropped. - self.0.send(Command::SystemCommand(command)).ok(); + self.system_sender.send(command).ok(); } } @@ -57,6 +89,6 @@ impl UICommandSender for CommandSender { /// Send a command to be executed. fn send_ui(&self, command: UICommand) { // The only way this can fail is if the receiver has been dropped. - self.0.send(Command::UICommand(command)).ok(); + self.ui_sender.send(command).ok(); } } diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index 67c1b6c7be91..a442af02f691 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -26,7 +26,7 @@ pub use annotations::{AnnotationMap, Annotations, ResolvedAnnotationInfo, MISSIN pub use app_options::AppOptions; pub use caches::{Cache, Caches}; pub use command_sender::{ - command_channel, Command, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender, + command_channel, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender, }; pub use component_ui_registry::{ComponentUiRegistry, UiVerbosity}; pub use item::{Item, ItemCollection}; diff --git a/crates/re_viewport/src/lib.rs b/crates/re_viewport/src/lib.rs index 330e04043621..90ec2b3ea70e 100644 --- a/crates/re_viewport/src/lib.rs +++ b/crates/re_viewport/src/lib.rs @@ -10,6 +10,7 @@ mod space_view_heuristics; mod space_view_highlights; mod view_category; mod viewport; +mod viewport_blueprint; pub mod blueprint_components; @@ -17,6 +18,7 @@ pub use space_info::SpaceInfoCollection; pub use space_view::SpaceViewBlueprint; pub use view_category::ViewCategory; pub use viewport::{Viewport, ViewportState}; +pub use viewport_blueprint::ViewportBlueprint; pub mod external { pub use re_space_view; diff --git a/crates/re_viewport/src/viewport_blueprint.rs b/crates/re_viewport/src/viewport_blueprint.rs new file mode 100644 index 000000000000..39c6bbd39b29 --- /dev/null +++ b/crates/re_viewport/src/viewport_blueprint.rs @@ -0,0 +1,290 @@ +use std::collections::BTreeMap; + +use ahash::HashMap; +use arrow2_convert::field::ArrowField; +use re_data_store::StoreDb; +use re_log_types::{ + Component, DataCell, DataRow, EntityPath, RowId, SerializableComponent, TimePoint, +}; +use re_viewer_context::{CommandSender, Item, SpaceViewId, SystemCommand, SystemCommandSender}; + +use crate::{ + blueprint_components::{ + AutoSpaceViews, SpaceViewComponent, SpaceViewMaximized, SpaceViewVisibility, + ViewportLayout, VIEWPORT_PATH, + }, + SpaceViewBlueprint, Viewport, +}; + +// ---------------------------------------------------------------------------- + +/// Defines the layout of the Viewport +#[derive(Clone)] +pub struct ViewportBlueprint<'a> { + pub blueprint_db: &'a StoreDb, + + pub viewport: Viewport, + snapshot: Viewport, +} + +impl<'a> ViewportBlueprint<'a> { + pub fn from_db(blueprint_db: &'a re_data_store::StoreDb) -> Self { + let space_views: HashMap = if let Some(space_views) = + blueprint_db + .entity_db + .tree + .children + .get(&re_data_store::EntityPathPart::Name( + SpaceViewComponent::SPACEVIEW_PREFIX.into(), + )) { + space_views + .children + .values() + .filter_map(|view_tree| load_space_view(&view_tree.path, blueprint_db)) + .map(|sv| (sv.id, sv)) + .collect() + } else { + Default::default() + }; + + let viewport = load_viewport(blueprint_db, space_views); + + Self { + blueprint_db, + viewport: viewport.clone(), + snapshot: viewport, + } + } + + /// If `false`, the item is referring to data that is not present in this blueprint. + pub fn is_item_valid(&self, item: &Item) -> bool { + match item { + Item::ComponentPath(_) => true, + Item::InstancePath(space_view_id, _) => space_view_id + .map(|space_view_id| self.viewport.space_view(&space_view_id).is_some()) + .unwrap_or(true), + Item::SpaceView(space_view_id) => self.viewport.space_view(space_view_id).is_some(), + Item::DataBlueprintGroup(space_view_id, data_blueprint_group_handle) => { + if let Some(space_view) = self.viewport.space_view(space_view_id) { + space_view + .data_blueprint + .group(*data_blueprint_group_handle) + .is_some() + } else { + false + } + } + } + } + + pub fn sync_changes(&self, command_sender: &CommandSender) { + let mut deltas = vec![]; + + sync_viewport(&mut deltas, &self.viewport, &self.snapshot); + + // Add any new or modified space views + for id in self.viewport.space_view_ids() { + if let Some(space_view) = self.viewport.space_view(id) { + sync_space_view(&mut deltas, space_view, self.snapshot.space_view(id)); + } + } + + // Remove any deleted space views + for space_view_id in self.snapshot.space_view_ids() { + if self.viewport.space_view(space_view_id).is_none() { + clear_space_view(&mut deltas, space_view_id); + } + } + + command_sender.send_system(SystemCommand::UpdateBlueprint( + self.blueprint_db.store_id().clone(), + deltas, + )); + } +} + +// ---------------------------------------------------------------------------- + +// TODO(jleibs): Move this helper to a better location +fn add_delta_from_single_component( + deltas: &mut Vec, + entity_path: &EntityPath, + timepoint: &TimePoint, + component: C, +) { + let row = DataRow::from_cells1_sized( + RowId::random(), + entity_path.clone(), + timepoint.clone(), + 1, + [component].as_slice(), + ); + + deltas.push(row); +} + +// ---------------------------------------------------------------------------- + +fn load_space_view( + path: &EntityPath, + blueprint_db: &re_data_store::StoreDb, +) -> Option { + blueprint_db + .store() + .query_timeless_component::(path) + .map(|c| c.space_view) +} + +fn load_viewport( + blueprint_db: &re_data_store::StoreDb, + space_views: HashMap, +) -> Viewport { + let auto_space_views = blueprint_db + .store() + .query_timeless_component::(&VIEWPORT_PATH.into()) + .unwrap_or_else(|| { + // Only enable auto-space-views if this is the app-default blueprint + AutoSpaceViews( + blueprint_db + .store_info() + .map_or(false, |ri| ri.is_app_default_blueprint()), + ) + }); + + let space_view_visibility = blueprint_db + .store() + .query_timeless_component::(&VIEWPORT_PATH.into()) + .unwrap_or_default(); + + let space_view_maximized = blueprint_db + .store() + .query_timeless_component::(&VIEWPORT_PATH.into()) + .unwrap_or_default(); + + let viewport_layout: ViewportLayout = blueprint_db + .store() + .query_timeless_component::(&VIEWPORT_PATH.into()) + .unwrap_or_default(); + + let unknown_space_views: HashMap<_, _> = space_views + .iter() + .filter(|(k, _)| !viewport_layout.space_view_keys.contains(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + + let known_space_views: BTreeMap<_, _> = space_views + .into_iter() + .filter(|(k, _)| viewport_layout.space_view_keys.contains(k)) + .collect(); + + let mut viewport = Viewport { + space_views: known_space_views, + visible: space_view_visibility.0, + trees: viewport_layout.trees, + maximized: space_view_maximized.0, + has_been_user_edited: viewport_layout.has_been_user_edited, + auto_space_views: auto_space_views.0, + }; + // TODO(jleibs): It seems we shouldn't call this until later, after we've created + // the snapshot. Doing this here means we are mutating the state before it goes + // into the snapshot. For example, even if there's no visibility in the + // store, this will end up with default-visibility, which then *won't* be saved back. + for (_, view) in unknown_space_views { + viewport.add_space_view(view); + } + + viewport +} + +// ---------------------------------------------------------------------------- + +fn sync_space_view( + deltas: &mut Vec, + space_view: &SpaceViewBlueprint, + snapshot: Option<&SpaceViewBlueprint>, +) { + if Some(space_view) != snapshot { + let entity_path = EntityPath::from(format!( + "{}/{}", + SpaceViewComponent::SPACEVIEW_PREFIX, + space_view.id + )); + + // TODO(jleibs): Seq instead of timeless? + let timepoint = TimePoint::timeless(); + + let component = SpaceViewComponent { + space_view: space_view.clone(), + }; + + add_delta_from_single_component(deltas, &entity_path, &timepoint, component); + } +} + +fn clear_space_view(deltas: &mut Vec, space_view_id: &SpaceViewId) { + let entity_path = EntityPath::from(format!( + "{}/{}", + SpaceViewComponent::SPACEVIEW_PREFIX, + space_view_id + )); + + // TODO(jleibs): Seq instead of timeless? + let timepoint = TimePoint::timeless(); + + let cell = + DataCell::from_arrow_empty(SpaceViewComponent::name(), SpaceViewComponent::data_type()); + + let row = DataRow::from_cells1_sized(RowId::random(), entity_path, timepoint, 0, cell); + + deltas.push(row); +} + +fn sync_viewport(deltas: &mut Vec, viewport: &Viewport, snapshot: &Viewport) { + let entity_path = EntityPath::from(VIEWPORT_PATH); + + // TODO(jleibs): Seq instead of timeless? + let timepoint = TimePoint::timeless(); + + if viewport.auto_space_views != snapshot.auto_space_views { + let component = AutoSpaceViews(viewport.auto_space_views); + add_delta_from_single_component(deltas, &entity_path, &timepoint, component); + } + + if viewport.visible != snapshot.visible { + let component = SpaceViewVisibility(viewport.visible.clone()); + add_delta_from_single_component(deltas, &entity_path, &timepoint, component); + } + + if viewport.maximized != snapshot.maximized { + let component = SpaceViewMaximized(viewport.maximized); + add_delta_from_single_component(deltas, &entity_path, &timepoint, component); + } + + // Note: we can't just check `viewport.trees != snapshot.trees` because the + // tree contains serde[skip]'d state that won't match in PartialEq. + if viewport.trees.len() != snapshot.trees.len() + || !viewport.trees.iter().zip(snapshot.trees.iter()).all( + |((left_vis, left_tree), (right_vis, right_tree))| { + left_vis == right_vis + && left_tree.root == right_tree.root + && left_tree.tiles.tiles == right_tree.tiles.tiles + }, + ) + || viewport.has_been_user_edited != snapshot.has_been_user_edited + { + let component = ViewportLayout { + space_view_keys: viewport.space_views.keys().cloned().collect(), + trees: viewport.trees.clone(), + has_been_user_edited: viewport.has_been_user_edited, + }; + + add_delta_from_single_component(deltas, &entity_path, &timepoint, component); + + // TODO(jleibs): Sort out this causality mess + // If we are saving a new layout, we also need to save the visibility-set because + // it gets mutated on load but isn't guaranteed to be mutated on layout-change + // which means it won't get saved. + let component = SpaceViewVisibility(viewport.visible.clone()); + add_delta_from_single_component(deltas, &entity_path, &timepoint, component); + } +}