diff --git a/crates/re_space_view/src/data_query_blueprint.rs b/crates/re_space_view/src/data_query_blueprint.rs index 8139b2f1e45f..d9c6cb2340a9 100644 --- a/crates/re_space_view/src/data_query_blueprint.rs +++ b/crates/re_space_view/src/data_query_blueprint.rs @@ -4,6 +4,7 @@ use re_data_store::{ EntityProperties, EntityPropertiesComponent, EntityPropertyMap, EntityTree, StoreDb, }; use re_log_types::{DataRow, EntityPath, EntityPathExpr, RowId, TimePoint}; +use re_types_core::archetypes::Clear; use re_viewer_context::{ DataQueryId, DataQueryResult, DataResult, DataResultHandle, DataResultNode, DataResultTree, EntitiesPerSystem, EntitiesPerSystemPerClass, SpaceViewClassIdentifier, SpaceViewId, @@ -28,7 +29,14 @@ use crate::{ /// The results of recursive expressions are only included if they are found within the [`EntityTree`] /// and for which there is a valid `ViewPart` system. This keeps recursive expressions from incorrectly /// picking up irrelevant data within the tree. -#[derive(Clone, PartialEq, Eq)] +/// +/// Note: [`DataQueryBlueprint`] doesn't implement Clone because it stores an internal +/// uuid used for identifying the path of its data in the blueprint store. It's ambiguous +/// whether the intent is for a clone to write to the same place. +/// +/// If you want a new space view otherwise identical to an existing one, use +/// [`DataQueryBlueprint::duplicate`]. +#[derive(PartialEq, Eq)] pub struct DataQueryBlueprint { pub id: DataQueryId, pub space_view_class_identifier: SpaceViewClassIdentifier, @@ -47,6 +55,10 @@ impl DataQueryBlueprint { pub const INDIVIDUAL_OVERRIDES_PREFIX: &'static str = "individual_overrides"; pub const RECURSIVE_OVERRIDES_PREFIX: &'static str = "recursive_overrides"; + /// Creates a new [`DataQueryBlueprint`]. + /// + /// This [`DataQueryBlueprint`] is ephemeral. It must be saved by calling + /// `save_to_blueprint_store` on the enclosing `SpaceViewBlueprint`. pub fn new( space_view_class_identifier: SpaceViewClassIdentifier, queries_entities: impl Iterator, @@ -58,18 +70,17 @@ impl DataQueryBlueprint { } } + /// Attempt to load a [`DataQueryBlueprint`] from the blueprint store. pub fn try_from_db( - path: &EntityPath, + id: DataQueryId, blueprint_db: &StoreDb, space_view_class_identifier: SpaceViewClassIdentifier, ) -> Option { let expressions = blueprint_db .store() - .query_timeless_component::(path) + .query_timeless_component::(&id.as_entity_path()) .map(|c| c.value)?; - let id = DataQueryId::from_entity_path(path); - Some(Self { id, space_view_class_identifier, @@ -77,6 +88,30 @@ impl DataQueryBlueprint { }) } + /// Persist the entire [`DataQueryBlueprint`] to the blueprint store. + /// + /// This only needs to be called if the [`DataQueryBlueprint`] was created with [`Self::new`]. + /// + /// Otherwise, incremental calls to `set_` functions will write just the necessary component + /// update directly to the store. + pub fn save_to_blueprint_store(&self, ctx: &ViewerContext<'_>) { + ctx.save_blueprint_component(&self.id.as_entity_path(), self.expressions.clone()); + } + + /// Creates a new [`DataQueryBlueprint`] with a the same contents, but a different [`DataQueryId`] + pub fn duplicate(&self) -> Self { + Self { + id: DataQueryId::random(), + space_view_class_identifier: self.space_view_class_identifier, + expressions: self.expressions.clone(), + } + } + + pub fn clear(&self, ctx: &ViewerContext<'_>) { + let clear = Clear::recursive(); + ctx.save_blueprint_component(&self.id.as_entity_path(), clear.is_recursive); + } + pub fn build_resolver<'a>( &self, container: SpaceViewId, diff --git a/crates/re_types/definitions/rerun/blueprint/archetypes/viewport_blueprint.fbs b/crates/re_types/definitions/rerun/blueprint/archetypes/viewport_blueprint.fbs index ae3f10714f4a..00bc5911d179 100644 --- a/crates/re_types/definitions/rerun/blueprint/archetypes/viewport_blueprint.fbs +++ b/crates/re_types/definitions/rerun/blueprint/archetypes/viewport_blueprint.fbs @@ -19,11 +19,11 @@ table ViewportBlueprint ( /// All of the space-views that belong to the viewport. space_views: rerun.blueprint.components.IncludedSpaceViews ("attr.rerun.component_required", order: 1000); - /// The layout of the space-views - layout: rerun.blueprint.components.ViewportLayout ("attr.rerun.component_required", order: 2000); - // --- Optional --- + /// The layout of the space-views + layout: rerun.blueprint.components.ViewportLayout ("attr.rerun.component_optional", nullable, order: 2000); + /// Show one tab as maximized? maximized: rerun.blueprint.components.SpaceViewMaximized ("attr.rerun.component_optional", nullable, order: 3000); diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index cb7526bb035f..ca8bb98a6d36 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -368,6 +368,7 @@ impl App { SystemCommand::ResetBlueprint => { // By clearing the blueprint it will be re-populated with the defaults // at the beginning of the next frame. + re_log::debug!("Reset blueprint"); store_hub.clear_blueprint(); } SystemCommand::UpdateBlueprint(blueprint_id, updates) => { diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 1d5fbaf08672..fad212e89915 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -6,10 +6,11 @@ use re_smart_channel::ReceiveSet; use re_space_view::DataQuery as _; use re_viewer_context::{ AppOptions, Caches, CommandSender, ComponentUiRegistry, PlayState, RecordingConfig, - SelectionState, SpaceViewClassRegistry, StoreContext, ViewerContext, + SelectionState, SpaceViewClassRegistry, StoreContext, SystemCommandSender as _, ViewerContext, }; use re_viewport::{ - identify_entities_per_system_per_class, SpaceInfoCollection, Viewport, ViewportState, + identify_entities_per_system_per_class, SpaceInfoCollection, Viewport, ViewportBlueprint, + ViewportState, }; use crate::ui::recordings_panel_ui; @@ -104,7 +105,25 @@ impl AppState { viewport_state, } = self; - let mut viewport = Viewport::from_db(store_context.blueprint, viewport_state); + let viewport_blueprint = ViewportBlueprint::try_from_db(store_context.blueprint); + let mut viewport = Viewport::new( + &viewport_blueprint, + viewport_state, + space_view_class_registry, + ); + + // If the blueprint is invalid, reset it. + if viewport.blueprint.is_invalid() { + re_log::warn!("Incompatible blueprint detected. Resetting to default."); + command_sender.send_system(re_viewer_context::SystemCommand::ResetBlueprint); + + // The blueprint isn't valid so nothing past this is going to work properly. + // we might as well return and it will get fixed on the next frame. + + // TODO(jleibs): If we move viewport loading up to a context where the StoreDb is mutable + // we can run the clear and re-load. + return; + } recording_config_entry(recording_configs, store_db.store_id().clone(), store_db) .selection_state @@ -125,11 +144,11 @@ impl AppState { viewport .blueprint .space_views - .values_mut() + .values() .flat_map(|space_view| { space_view.queries.iter().map(|query| { - let resolver = - query.build_resolver(space_view.id, &space_view.auto_properties); + let props = viewport.state.space_view_props(space_view.id); + let resolver = query.build_resolver(space_view.id, props); ( query.id, query.execute_query( @@ -163,12 +182,6 @@ impl AppState { // have the latest information. let spaces_info = SpaceInfoCollection::new(ctx.store_db); - // If the blueprint is invalid, reset it. - if viewport.blueprint.is_invalid() { - re_log::warn!("Incompatible blueprint detected. Resetting to default."); - viewport.blueprint.reset(&ctx, &spaces_info); - } - viewport.on_frame_start(&ctx, &spaces_info); // TODO(jleibs): Running the queries a second time is annoying, but we need @@ -179,11 +192,11 @@ impl AppState { viewport .blueprint .space_views - .values_mut() + .values() .flat_map(|space_view| { space_view.queries.iter().map(|query| { - let resolver = - query.build_resolver(space_view.id, &space_view.auto_properties); + let props = viewport.state.space_view_props(space_view.id); + let resolver = query.build_resolver(space_view.id, props); ( query.id, query.execute_query( @@ -243,7 +256,7 @@ impl AppState { ui.add_space(4.0); } - blueprint_panel_ui(&mut viewport.blueprint, &ctx, ui, &spaces_info); + blueprint_panel_ui(&mut viewport, &ctx, ui, &spaces_info); }, ); @@ -266,7 +279,8 @@ impl AppState { }); }); - viewport.sync_blueprint_changes(command_sender); + // Process deferred layout operations and apply updates back to blueprint + viewport.update_and_sync_tile_tree_to_blueprint(&ctx); { // We move the time at the very end of the frame, diff --git a/crates/re_viewer/src/ui/blueprint_panel.rs b/crates/re_viewer/src/ui/blueprint_panel.rs index b7fae9404540..9538c93290c3 100644 --- a/crates/re_viewer/src/ui/blueprint_panel.rs +++ b/crates/re_viewer/src/ui/blueprint_panel.rs @@ -1,9 +1,9 @@ use re_viewer_context::{SystemCommandSender as _, ViewerContext}; -use re_viewport::{SpaceInfoCollection, ViewportBlueprint}; +use re_viewport::{SpaceInfoCollection, Viewport}; -/// Show the Blueprint section of the left panel based on the current [`ViewportBlueprint`] +/// Show the Blueprint section of the left panel based on the current [`Viewport`] pub fn blueprint_panel_ui( - blueprint: &mut ViewportBlueprint<'_>, + viewport: &mut Viewport<'_, '_>, ctx: &ViewerContext<'_>, ui: &mut egui::Ui, spaces_info: &SpaceInfoCollection, @@ -14,7 +14,7 @@ pub fn blueprint_panel_ui( "Blueprint", Some("The Blueprint is where you can configure the Rerun Viewer"), |ui| { - blueprint.add_new_spaceview_button_ui(ctx, ui, spaces_info); + viewport.add_new_spaceview_button_ui(ctx, ui, spaces_info); reset_blueprint_button_ui(ctx, ui); }, ); @@ -22,7 +22,7 @@ pub fn blueprint_panel_ui( // This call is excluded from `panel_content` because it has a ScrollArea, which should not be // inset. Instead, it calls panel_content itself inside the ScrollArea. - blueprint.tree_ui(ctx, ui); + viewport.tree_ui(ctx, ui); } fn reset_blueprint_button_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) { diff --git a/crates/re_viewer/src/ui/selection_history_ui.rs b/crates/re_viewer/src/ui/selection_history_ui.rs index b7e00769bc18..76845c17fe19 100644 --- a/crates/re_viewer/src/ui/selection_history_ui.rs +++ b/crates/re_viewer/src/ui/selection_history_ui.rs @@ -15,7 +15,7 @@ impl SelectionHistoryUi { &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, - blueprint: &ViewportBlueprint<'_>, + blueprint: &ViewportBlueprint, history: &mut SelectionHistory, ) -> Option { let next = self.next_button_ui(re_ui, ui, blueprint, history); @@ -27,7 +27,7 @@ impl SelectionHistoryUi { &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, - blueprint: &ViewportBlueprint<'_>, + blueprint: &ViewportBlueprint, history: &mut SelectionHistory, ) -> Option { // undo selection @@ -77,7 +77,7 @@ impl SelectionHistoryUi { &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, - blueprint: &ViewportBlueprint<'_>, + blueprint: &ViewportBlueprint, history: &mut SelectionHistory, ) -> Option { // redo selection @@ -126,7 +126,7 @@ impl SelectionHistoryUi { #[allow(clippy::unused_self)] fn history_item_ui( &mut self, - blueprint: &ViewportBlueprint<'_>, + blueprint: &ViewportBlueprint, ui: &mut egui::Ui, index: usize, history: &mut SelectionHistory, @@ -157,7 +157,7 @@ fn item_kind_ui(ui: &mut egui::Ui, sel: &Item) { ui.weak(RichText::new(format!("({})", sel.kind()))); } -fn item_collection_to_string(blueprint: &ViewportBlueprint<'_>, 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()) @@ -168,7 +168,7 @@ fn item_collection_to_string(blueprint: &ViewportBlueprint<'_>, items: &ItemColl } } -fn item_to_string(blueprint: &ViewportBlueprint<'_>, item: &Item) -> String { +fn item_to_string(blueprint: &ViewportBlueprint, item: &Item) -> String { match item { Item::SpaceView(sid) => { if let Some(space_view) = blueprint.space_view(sid) { diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_viewer/src/ui/selection_panel.rs index 276373f2f6ba..227656fa51b3 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_viewer/src/ui/selection_panel.rs @@ -72,7 +72,7 @@ impl SelectionPanel { if let Some(selection) = self.selection_state_ui.selection_ui( ctx.re_ui, ui, - &viewport.blueprint, + viewport.blueprint, &mut history, ) { ctx.selection_state() @@ -122,20 +122,15 @@ impl SelectionPanel { }; for (i, item) in selection.iter().enumerate() { ui.push_id(i, |ui| { - what_is_selected_ui(ui, ctx, &mut viewport.blueprint, item); + what_is_selected_ui(ui, ctx, viewport.blueprint, item); match item { Item::Container(tile_id) => { - container_top_level_properties(ui, ctx, &mut viewport.blueprint, tile_id); + container_top_level_properties(ui, ctx, viewport, tile_id); } Item::SpaceView(space_view_id) => { - space_view_top_level_properties( - ui, - ctx, - &mut viewport.blueprint, - space_view_id, - ); + space_view_top_level_properties(ui, ctx, viewport.blueprint, space_view_id); } _ => {} @@ -197,7 +192,7 @@ fn space_view_button( fn what_is_selected_ui( ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - viewport: &mut ViewportBlueprint<'_>, + viewport: &ViewportBlueprint, item: &Item, ) { match item { @@ -236,7 +231,7 @@ fn what_is_selected_ui( list_existing_data_blueprints(ui, ctx, entity_path, viewport); } Item::SpaceView(space_view_id) => { - if let Some(space_view) = viewport.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.space_view(space_view_id) { let space_view_class = space_view.class(ctx.space_view_class_registry); item_title_ui( ctx.re_ui, @@ -259,7 +254,7 @@ fn what_is_selected_ui( }; if let Some(space_view_id) = space_view_id { - if let Some(space_view) = viewport.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.space_view(space_view_id) { item_title_ui( ctx.re_ui, ui, @@ -334,7 +329,7 @@ fn list_existing_data_blueprints( ui: &mut egui::Ui, ctx: &ViewerContext<'_>, entity_path: &EntityPath, - blueprint: &ViewportBlueprint<'_>, + blueprint: &ViewportBlueprint, ) { let space_views_with_path = blueprint.space_views_containing_entity_path(ctx, entity_path); @@ -367,20 +362,23 @@ fn list_existing_data_blueprints( fn space_view_top_level_properties( ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - viewport: &mut ViewportBlueprint<'_>, + viewport: &ViewportBlueprint, space_view_id: &SpaceViewId, ) { - if let Some(space_view) = viewport.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.space_view(space_view_id) { egui::Grid::new("space_view_top_level_properties") .num_columns(2) .show(ui, |ui| { + let mut name = space_view.display_name.clone(); ui.label("Name").on_hover_text( "The name of the Space View used for display purposes. This can be any text \ string.", ); - ui.text_edit_singleline(&mut space_view.display_name); + ui.text_edit_singleline(&mut name); ui.end_row(); + space_view.set_display_name(name, ctx); + ui.label("Space origin").on_hover_text( "The origin Entity for this Space View. For spatial Space Views, the Space \ View's origin is the same as this Entity's origin and all transforms are \ @@ -409,7 +407,7 @@ fn space_view_top_level_properties( fn container_top_level_properties( ui: &mut egui::Ui, _ctx: &ViewerContext<'_>, - viewport: &mut ViewportBlueprint<'_>, + viewport: &mut Viewport<'_, '_>, tile_id: &egui_tiles::TileId, ) { if let Some(Tile::Container(container)) = viewport.tree.tiles.get_mut(*tile_id) { @@ -472,6 +470,7 @@ fn container_top_level_properties( GridLayout::Auto, grid_layout_to_string(&GridLayout::Auto), ); + ui.separator(); for columns in 1..=grid.num_children() { @@ -522,15 +521,14 @@ fn blueprint_ui( .clicked() { if let Some(space_view) = viewport.blueprint.space_view(space_view_id) { - let mut new_space_view = space_view.clone(); - new_space_view.id = SpaceViewId::random(); - viewport.blueprint.add_space_view(new_space_view); - viewport.blueprint.mark_user_interaction(); + let new_space_view = space_view.duplicate(); + viewport.blueprint.add_space_views(std::iter::once(new_space_view), ctx, &mut viewport.deferred_tree_actions); + viewport.blueprint.mark_user_interaction(ctx); } } }); - if let Some(space_view) = viewport.blueprint.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.blueprint.space_view(space_view_id) { if let Some(query) = space_view.queries.first() { let inclusions = query.expressions.inclusions.join("\n"); let mut edited_inclusions = inclusions.clone(); @@ -578,15 +576,16 @@ fn blueprint_ui( vec![row], )); - space_view.entities_determined_by_user = true; + space_view.set_entity_determined_by_user(ctx); } } } ui.add_space(ui.spacing().item_spacing.y); - if let Some(space_view) = viewport.blueprint.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.blueprint.space_view(space_view_id) { let space_view_class = *space_view.class_identifier(); + let space_view_state = viewport.state.space_view_state_mut( ctx.space_view_class_registry, space_view.id, @@ -615,7 +614,7 @@ fn blueprint_ui( .selection_ui( ctx, ui, - space_view_state, + space_view_state.space_view_state.as_mut(), &space_view.space_origin, space_view.id, &mut props, @@ -643,7 +642,7 @@ fn blueprint_ui( Item::InstancePath(space_view_id, instance_path) => { if let Some(space_view_id) = space_view_id { - if let Some(space_view) = viewport.blueprint.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.blueprint.space_view(space_view_id) { if instance_path.instance_key.is_specific() { ui.horizontal(|ui| { ui.label("Part of"); @@ -687,7 +686,7 @@ fn blueprint_ui( } Item::DataBlueprintGroup(space_view_id, query_id, group_path) => { - if let Some(space_view) = viewport.blueprint.space_view_mut(space_view_id) { + if let Some(space_view) = viewport.blueprint.space_view(space_view_id) { let as_group = true; let query_result = ctx.lookup_query_result(*query_id); diff --git a/crates/re_viewer_context/src/blueprint_helpers.rs b/crates/re_viewer_context/src/blueprint_helpers.rs new file mode 100644 index 000000000000..a16da5c5489b --- /dev/null +++ b/crates/re_viewer_context/src/blueprint_helpers.rs @@ -0,0 +1,33 @@ +use re_log_types::{DataRow, EntityPath, RowId, TimePoint}; + +use crate::{SystemCommand, SystemCommandSender as _, ViewerContext}; + +impl ViewerContext<'_> { + /// Helper to save a component to the blueprint store. + pub fn save_blueprint_component<'a, C>(&self, entity_path: &EntityPath, component: C) + where + C: re_types::Component + Clone + 'a, + std::borrow::Cow<'a, C>: std::convert::From, + { + let timepoint = TimePoint::timeless(); + + match DataRow::from_cells1_sized( + RowId::new(), + entity_path.clone(), + timepoint.clone(), + 1, + [component], + ) { + Ok(row) => self + .command_sender + .send_system(SystemCommand::UpdateBlueprint( + self.store_context.blueprint.store_id().clone(), + vec![row], + )), + Err(err) => { + // TODO(emilk): statically check that the component is a mono-component - then this cannot fail! + re_log::error_once!("Failed to create DataRow for blueprint component: {}", err); + } + } + } +} diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index aac2b9a87983..bc324310a5dc 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -4,6 +4,7 @@ mod annotations; mod app_options; +mod blueprint_helpers; mod blueprint_id; mod caches; mod command_sender; diff --git a/crates/re_viewport/src/blueprint/archetypes/viewport_blueprint.rs b/crates/re_viewport/src/blueprint/archetypes/viewport_blueprint.rs index 685539af5f5b..b43e2a73404b 100644 --- a/crates/re_viewport/src/blueprint/archetypes/viewport_blueprint.rs +++ b/crates/re_viewport/src/blueprint/archetypes/viewport_blueprint.rs @@ -28,7 +28,7 @@ pub struct ViewportBlueprint { pub space_views: crate::blueprint::components::IncludedSpaceViews, /// The layout of the space-views - pub layout: crate::blueprint::components::ViewportLayout, + pub layout: Option, /// Show one tab as maximized? pub maximized: Option, @@ -42,23 +42,19 @@ pub struct ViewportBlueprint { pub auto_space_views: Option, } -static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 2usize]> = - once_cell::sync::Lazy::new(|| { - [ - "rerun.blueprint.components.IncludedSpaceViews".into(), - "rerun.blueprint.components.ViewportLayout".into(), - ] - }); +static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.blueprint.components.IncludedSpaceViews".into()]); static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = once_cell::sync::Lazy::new(|| ["rerun.blueprint.components.ViewportBlueprintIndicator".into()]); -static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 4usize]> = +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 5usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.blueprint.components.AutoLayout".into(), "rerun.blueprint.components.AutoSpaceViews".into(), "rerun.blueprint.components.SpaceViewMaximized".into(), + "rerun.blueprint.components.ViewportLayout".into(), "rerun.components.InstanceKey".into(), ] }); @@ -67,11 +63,11 @@ static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 7usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.blueprint.components.IncludedSpaceViews".into(), - "rerun.blueprint.components.ViewportLayout".into(), "rerun.blueprint.components.ViewportBlueprintIndicator".into(), "rerun.blueprint.components.AutoLayout".into(), "rerun.blueprint.components.AutoSpaceViews".into(), "rerun.blueprint.components.SpaceViewMaximized".into(), + "rerun.blueprint.components.ViewportLayout".into(), "rerun.components.InstanceKey".into(), ] }); @@ -140,19 +136,20 @@ impl ::re_types_core::Archetype for ViewportBlueprint { .ok_or_else(DeserializationError::missing_data) .with_context("rerun.blueprint.archetypes.ViewportBlueprint#space_views")? }; - let layout = { - let array = arrays_by_name - .get("rerun.blueprint.components.ViewportLayout") - .ok_or_else(DeserializationError::missing_data) - .with_context("rerun.blueprint.archetypes.ViewportBlueprint#layout")?; - ::from_arrow_opt(&**array) - .with_context("rerun.blueprint.archetypes.ViewportBlueprint#layout")? - .into_iter() - .next() - .flatten() - .ok_or_else(DeserializationError::missing_data) - .with_context("rerun.blueprint.archetypes.ViewportBlueprint#layout")? - }; + let layout = + if let Some(array) = arrays_by_name.get("rerun.blueprint.components.ViewportLayout") { + Some({ + ::from_arrow_opt(&**array) + .with_context("rerun.blueprint.archetypes.ViewportBlueprint#layout")? + .into_iter() + .next() + .flatten() + .ok_or_else(DeserializationError::missing_data) + .with_context("rerun.blueprint.archetypes.ViewportBlueprint#layout")? + }) + } else { + None + }; let maximized = if let Some(array) = arrays_by_name.get("rerun.blueprint.components.SpaceViewMaximized") { @@ -214,7 +211,9 @@ impl ::re_types_core::AsComponents for ViewportBlueprint { [ Some(Self::indicator()), Some((&self.space_views as &dyn ComponentBatch).into()), - Some((&self.layout as &dyn ComponentBatch).into()), + self.layout + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), self.maximized .as_ref() .map(|comp| (comp as &dyn ComponentBatch).into()), @@ -237,19 +236,25 @@ impl ::re_types_core::AsComponents for ViewportBlueprint { } impl ViewportBlueprint { - pub fn new( - space_views: impl Into, - layout: impl Into, - ) -> Self { + pub fn new(space_views: impl Into) -> Self { Self { space_views: space_views.into(), - layout: layout.into(), + layout: None, maximized: None, auto_layout: None, auto_space_views: None, } } + #[inline] + pub fn with_layout( + mut self, + layout: impl Into, + ) -> Self { + self.layout = Some(layout.into()); + self + } + #[inline] pub fn with_maximized( mut self, diff --git a/crates/re_viewport/src/space_view.rs b/crates/re_viewport/src/space_view.rs index 595ab57ccf2b..e99bf1bae77f 100644 --- a/crates/re_viewport/src/space_view.rs +++ b/crates/re_viewport/src/space_view.rs @@ -1,17 +1,19 @@ -use ahash::HashSet; use re_arrow_store::LatestAtQuery; use re_data_store::{EntityPath, EntityProperties, StoreDb, TimeInt, VisibleHistory}; use re_data_store::{EntityPropertiesComponent, EntityPropertyMap}; -use re_log_types::{EntityPathExpr, Timeline}; +use re_log_types::{DataRow, EntityPathExpr, RowId, TimePoint, Timeline}; use re_query::query_archetype; use re_renderer::ScreenshotProcessor; use re_space_view::{DataQueryBlueprint, ScreenshotMode}; use re_space_view_time_series::TimeSeriesSpaceView; +use re_types::blueprint::components::{EntitiesDeterminedByUser, Name, SpaceViewOrigin}; +use re_types_core::archetypes::Clear; use re_viewer_context::{ DataQueryId, DataResult, DynSpaceViewClass, PerSystemDataResults, PerSystemEntities, SpaceViewClass, SpaceViewClassIdentifier, SpaceViewHighlights, SpaceViewId, SpaceViewState, - StoreContext, SystemExecutionOutput, ViewQuery, ViewerContext, + StoreContext, SystemCommand, SystemCommandSender as _, SystemExecutionOutput, ViewQuery, + ViewerContext, }; use crate::system_execution::create_and_run_space_view_systems; @@ -19,7 +21,13 @@ use crate::system_execution::create_and_run_space_view_systems; // ---------------------------------------------------------------------------- /// A view of a space. -#[derive(Clone)] +/// +/// Note: [`SpaceViewBlueprint`] doesn't implement Clone because it stores an internal +/// uuid used for identifying the path of its data in the blueprint store. It's ambiguous +/// whether the intent is for a clone to write to the same place. +/// +/// If you want a new space view otherwise identical to an existing one, use +/// [`SpaceViewBlueprint::duplicate`]. pub struct SpaceViewBlueprint { pub id: SpaceViewId, pub display_name: String, @@ -36,36 +44,13 @@ pub struct SpaceViewBlueprint { /// True if the user is expected to add entities themselves. False otherwise. pub entities_determined_by_user: bool, - - /// Auto Properties - // TODO(jleibs): This needs to be per-query - pub auto_properties: EntityPropertyMap, -} - -/// Determine whether this `SpaceViewBlueprint` has user-edits relative to another `SpaceViewBlueprint` -impl SpaceViewBlueprint { - pub fn has_edits(&self, other: &Self) -> bool { - let Self { - id, - display_name, - class_identifier, - space_origin, - queries, - entities_determined_by_user, - auto_properties: _, - } = self; - - id != &other.id - || display_name != &other.display_name - || class_identifier != &other.class_identifier - || space_origin != &other.space_origin - || queries.iter().map(|q| q.id).collect::>() - != other.queries.iter().map(|q| q.id).collect::>() - || entities_determined_by_user != &other.entities_determined_by_user - } } impl SpaceViewBlueprint { + /// Creates a new [`SpaceViewBlueprint`] with a single [`DataQueryBlueprint`]. + /// + /// This [`SpaceViewBlueprint`] is ephemeral. If you want to make it permanent you + /// must call [`Self::save_to_blueprint_store`]. pub fn new( space_view_class: SpaceViewClassIdentifier, space_view_class_display_name: &str, @@ -92,11 +77,11 @@ impl SpaceViewBlueprint { space_origin: space_path.clone(), queries: vec![query], entities_determined_by_user: false, - auto_properties: Default::default(), } } - pub fn try_from_db(path: &EntityPath, blueprint_db: &StoreDb) -> Option { + /// Attempt to load a [`SpaceViewBlueprint`] from the blueprint store. + pub fn try_from_db(id: SpaceViewId, blueprint_db: &StoreDb) -> Option { re_tracing::profile_function!(); let query = LatestAtQuery::latest(Timeline::default()); @@ -107,7 +92,7 @@ impl SpaceViewBlueprint { space_origin, entities_determined_by_user, contents, - } = query_archetype(blueprint_db.store(), &query, path) + } = query_archetype(blueprint_db.store(), &query, &id.as_entity_path()) .and_then(|arch| arch.to_archetype()) .map_err(|err| { if cfg!(debug_assertions) { @@ -119,8 +104,6 @@ impl SpaceViewBlueprint { }) .ok()?; - let id = SpaceViewId::from_entity_path(path); - let space_origin = space_origin.map_or_else(EntityPath::root, |origin| origin.0.into()); let class_identifier: SpaceViewClassIdentifier = class_identifier.0.as_str().into(); @@ -142,13 +125,7 @@ impl SpaceViewBlueprint { .0 .into_iter() .map(DataQueryId::from) - .filter_map(|id| { - DataQueryBlueprint::try_from_db( - &id.as_entity_path(), - blueprint_db, - class_identifier, - ) - }) + .filter_map(|id| DataQueryBlueprint::try_from_db(id, blueprint_db, class_identifier)) .collect(); let entities_determined_by_user = entities_determined_by_user.unwrap_or_default().0; @@ -160,10 +137,92 @@ impl SpaceViewBlueprint { space_origin, queries, entities_determined_by_user, - auto_properties: Default::default(), }) } + /// Persist the entire [`SpaceViewBlueprint`] to the blueprint store. + /// + /// This only needs to be called if the [`SpaceViewBlueprint`] was created with [`Self::new`]. + /// + /// Otherwise, incremental calls to `set_` functions will write just the necessary component + /// update directly to the store. + pub fn save_to_blueprint_store(&self, ctx: &ViewerContext<'_>) { + let timepoint = TimePoint::timeless(); + + let arch = re_types::blueprint::archetypes::SpaceViewBlueprint::new( + self.class_identifier().as_str(), + ) + .with_display_name(self.display_name.clone()) + .with_space_origin(&self.space_origin) + .with_entities_determined_by_user(self.entities_determined_by_user) + .with_contents(self.queries.iter().map(|q| q.id)); + + let mut deltas = vec![]; + + if let Ok(row) = + DataRow::from_archetype(RowId::new(), timepoint.clone(), self.entity_path(), &arch) + { + deltas.push(row); + } + + for query in &self.queries { + query.save_to_blueprint_store(ctx); + } + + ctx.command_sender + .send_system(SystemCommand::UpdateBlueprint( + ctx.store_context.blueprint.store_id().clone(), + deltas, + )); + } + + /// Creates a new [`SpaceViewBlueprint`] with a the same contents, but a different [`SpaceViewId`] + /// + /// Also duplicates all of the queries in the space view. + pub fn duplicate(&self) -> Self { + Self { + id: SpaceViewId::random(), + display_name: self.display_name.clone(), + class_identifier: self.class_identifier, + space_origin: self.space_origin.clone(), + queries: self.queries.iter().map(|q| q.duplicate()).collect(), + entities_determined_by_user: self.entities_determined_by_user, + } + } + + pub fn clear(&self, ctx: &ViewerContext<'_>) { + let clear = Clear::recursive(); + ctx.save_blueprint_component(&self.entity_path(), clear.is_recursive); + + for query in &self.queries { + query.clear(ctx); + } + } + + #[inline] + pub fn set_entity_determined_by_user(&self, ctx: &ViewerContext<'_>) { + if !self.entities_determined_by_user { + let component = EntitiesDeterminedByUser(true); + ctx.save_blueprint_component(&self.entity_path(), component); + } + } + + #[inline] + pub fn set_display_name(&self, name: String, ctx: &ViewerContext<'_>) { + if name != self.display_name { + let component = Name(name.into()); + ctx.save_blueprint_component(&self.entity_path(), component); + } + } + + #[inline] + pub fn set_origin(&self, origin: &EntityPath, ctx: &ViewerContext<'_>) { + if origin != &self.space_origin { + let component = SpaceViewOrigin(origin.into()); + ctx.save_blueprint_component(&self.entity_path(), component); + } + } + pub fn class_identifier(&self) -> &SpaceViewClassIdentifier { &self.class_identifier } @@ -175,7 +234,12 @@ impl SpaceViewBlueprint { space_view_class_registry.get_class_or_log_error(&self.class_identifier) } - pub fn on_frame_start(&mut self, ctx: &ViewerContext<'_>, view_state: &mut dyn SpaceViewState) { + pub fn on_frame_start( + &self, + ctx: &ViewerContext<'_>, + view_state: &mut dyn SpaceViewState, + view_props: &mut EntityPropertyMap, + ) { while ScreenshotProcessor::next_readback_result( ctx.render_ctx, self.id.gpu_readback_id(), @@ -207,7 +271,7 @@ impl SpaceViewBlueprint { ctx, view_state, &per_system_entities, - &mut self.auto_properties, + view_props, ); } @@ -362,26 +426,26 @@ impl SpaceViewBlueprint { } // TODO(jleibs): Get rid of mut by sending blueprint update - pub fn add_entity_exclusion(&mut self, ctx: &ViewerContext<'_>, expr: EntityPathExpr) { + pub fn add_entity_exclusion(&self, ctx: &ViewerContext<'_>, expr: EntityPathExpr) { if let Some(query) = self.queries.first() { query.add_entity_exclusion(ctx, expr); } - self.entities_determined_by_user = true; + self.set_entity_determined_by_user(ctx); } // TODO(jleibs): Get rid of mut by sending blueprint update - pub fn add_entity_inclusion(&mut self, ctx: &ViewerContext<'_>, expr: EntityPathExpr) { + pub fn add_entity_inclusion(&self, ctx: &ViewerContext<'_>, expr: EntityPathExpr) { if let Some(query) = self.queries.first() { query.add_entity_inclusion(ctx, expr); } - self.entities_determined_by_user = true; + self.set_entity_determined_by_user(ctx); } - pub fn clear_entity_expression(&mut self, ctx: &ViewerContext<'_>, expr: &EntityPathExpr) { + pub fn clear_entity_expression(&self, ctx: &ViewerContext<'_>, expr: &EntityPathExpr) { if let Some(query) = self.queries.first() { query.clear_entity_expression(ctx, expr); } - self.entities_determined_by_user = true; + self.set_entity_determined_by_user(ctx); } pub fn exclusions(&self) -> impl Iterator + '_ { @@ -449,6 +513,8 @@ mod tests { ), ); + let auto_properties = Default::default(); + let mut entities_per_system_per_class = EntitiesPerSystemPerClass::default(); entities_per_system_per_class .entry("3D".into()) @@ -465,7 +531,7 @@ mod tests { let query = space_view.queries.first().unwrap(); - let resolver = query.build_resolver(space_view.id, &space_view.auto_properties); + let resolver = query.build_resolver(space_view.id, &auto_properties); // No overrides set. Everybody has default values. { diff --git a/crates/re_viewport/src/space_view_entity_picker.rs b/crates/re_viewport/src/space_view_entity_picker.rs index c0146d1711d9..e8981f74512b 100644 --- a/crates/re_viewport/src/space_view_entity_picker.rs +++ b/crates/re_viewport/src/space_view_entity_picker.rs @@ -27,7 +27,7 @@ impl SpaceViewEntityPicker { &mut self, ctx: &ViewerContext<'_>, ui: &egui::Ui, - space_view: &mut SpaceViewBlueprint, + space_view: &SpaceViewBlueprint, ) -> bool { // This function fakes a modal window, since egui doesn't have them yet: https://github.com/emilk/egui/issues/686 @@ -83,11 +83,7 @@ impl SpaceViewEntityPicker { } } -fn add_entities_ui( - ctx: &ViewerContext<'_>, - ui: &mut egui::Ui, - space_view: &mut SpaceViewBlueprint, -) { +fn add_entities_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui, space_view: &SpaceViewBlueprint) { let spaces_info = SpaceInfoCollection::new(ctx.store_db); let tree = &ctx.store_db.tree(); let heuristic_context_per_entity = compute_heuristic_context_for_entities(ctx.store_db); @@ -123,7 +119,7 @@ fn add_entities_tree_ui( ui: &mut egui::Ui, name: &str, tree: &EntityTree, - space_view: &mut SpaceViewBlueprint, + space_view: &SpaceViewBlueprint, query_result: &DataQueryResult, inclusions: &HashSet, exclusions: &HashSet, @@ -192,7 +188,7 @@ fn add_entities_line_ui( ui: &mut egui::Ui, name: &str, entity_tree: &EntityTree, - space_view: &mut SpaceViewBlueprint, + space_view: &SpaceViewBlueprint, query_result: &DataQueryResult, inclusions: &HashSet, exclusions: &HashSet, diff --git a/crates/re_viewport/src/space_view_heuristics.rs b/crates/re_viewport/src/space_view_heuristics.rs index e98678ab9d7d..304ae1e572d0 100644 --- a/crates/re_viewport/src/space_view_heuristics.rs +++ b/crates/re_viewport/src/space_view_heuristics.rs @@ -314,35 +314,42 @@ pub fn default_created_space_views( // `AutoSpawnHeuristic::SpawnClassWithHighestScoreForRoot` means we're competing with other candidates for the same root. if let AutoSpawnHeuristic::SpawnClassWithHighestScoreForRoot(score) = spawn_heuristic { - let mut should_spawn_new = true; + // [`SpaceViewBlueprint`]s don't implement clone so wrap in an option so we can + // track whether or not we've consumed it. + let mut candidate_still_considered = Some(candidate); + for (prev_candidate, prev_spawn_heuristic) in &mut space_views { - if prev_candidate.space_origin == candidate.space_origin { - #[allow(clippy::match_same_arms)] - match prev_spawn_heuristic { - AutoSpawnHeuristic::SpawnClassWithHighestScoreForRoot(prev_score) => { - // If we're competing with a candidate for the same root, we either replace a lower score, or we yield. - should_spawn_new = false; - if *prev_score < score { - // Replace the previous candidate with this one. - *prev_candidate = candidate.clone(); - *prev_spawn_heuristic = spawn_heuristic; - } else { - // We have a lower score, so we don't spawn. + if let Some(candidate) = candidate_still_considered.take() { + if prev_candidate.space_origin == candidate.space_origin { + #[allow(clippy::match_same_arms)] + match prev_spawn_heuristic { + AutoSpawnHeuristic::SpawnClassWithHighestScoreForRoot(prev_score) => { + // If we're competing with a candidate for the same root, we either replace a lower score, or we yield. + if *prev_score < score { + // Replace the previous candidate with this one. + *prev_candidate = candidate; + *prev_spawn_heuristic = spawn_heuristic; + } + + // Either way we're done with this candidate. break; } - } - AutoSpawnHeuristic::AlwaysSpawn => { - // We can live side by side with always-spawn candidates. - } - AutoSpawnHeuristic::NeverSpawn => { - // Never spawn candidates should not be in the list, this is weird! - // But let's not fail on this since our heuristics are not perfect anyways. + AutoSpawnHeuristic::AlwaysSpawn => { + // We can live side by side with always-spawn candidates. + } + AutoSpawnHeuristic::NeverSpawn => { + // Never spawn candidates should not be in the list, this is weird! + // But let's not fail on this since our heuristics are not perfect anyways. + } } } + + // If we didn't hit the break condition, continue to consider the candidate + candidate_still_considered = Some(candidate); } } - if should_spawn_new { + if let Some(candidate) = candidate_still_considered { // Spatial views with images get extra treatment as well. if is_spatial_2d_class(candidate.class_identifier()) { #[derive(Hash, PartialEq, Eq)] diff --git a/crates/re_viewport/src/viewport.rs b/crates/re_viewport/src/viewport.rs index fa95c6ce8ce0..d4c33240a346 100644 --- a/crates/re_viewport/src/viewport.rs +++ b/crates/re_viewport/src/viewport.rs @@ -7,29 +7,38 @@ use std::collections::BTreeMap; use ahash::HashMap; use egui_tiles::Behavior as _; +use once_cell::sync::Lazy; +use re_data_store::EntityPropertyMap; use re_data_ui::item_ui; use re_ui::{Icon, ReUi}; use re_viewer_context::{ - CommandSender, Item, SpaceViewClassIdentifier, SpaceViewClassRegistry, SpaceViewId, - SpaceViewState, SystemExecutionOutput, ViewQuery, ViewerContext, + Item, SpaceViewClassIdentifier, SpaceViewClassRegistry, SpaceViewId, SpaceViewState, + SystemExecutionOutput, ViewQuery, ViewerContext, }; use crate::{ space_view_entity_picker::SpaceViewEntityPicker, space_view_heuristics::default_created_space_views, space_view_highlights::highlights_for_space_view, - system_execution::execute_systems_for_space_views, viewport_blueprint::load_viewport_blueprint, - SpaceInfoCollection, SpaceViewBlueprint, ViewportBlueprint, + system_execution::execute_systems_for_space_views, SpaceInfoCollection, SpaceViewBlueprint, + ViewportBlueprint, }; +// State for each `SpaceView` including both the auto properties and +// the internal state of the space view itself. +pub struct PerSpaceViewState { + pub auto_properties: EntityPropertyMap, + pub space_view_state: Box, +} + // ---------------------------------------------------------------------------- /// State for the [`Viewport`] that persists across frames but otherwise /// is not saved. #[derive(Default)] pub struct ViewportState { pub(crate) space_view_entity_window: Option, - space_view_states: HashMap>, + space_view_states: HashMap, /// List of all space views that were visible *on screen* (excluding e.g. unselected tabs) the last frame. /// @@ -37,64 +46,92 @@ pub struct ViewportState { space_views_displayed_last_frame: Vec, } +static DEFAULT_PROPS: Lazy = Lazy::::new(Default::default); + impl ViewportState { pub fn space_view_state_mut( &mut self, space_view_class_registry: &SpaceViewClassRegistry, space_view_id: SpaceViewId, space_view_class: &SpaceViewClassIdentifier, - ) -> &mut dyn SpaceViewState { + ) -> &mut PerSpaceViewState { self.space_view_states .entry(space_view_id) - .or_insert_with(|| { - space_view_class_registry + .or_insert_with(|| PerSpaceViewState { + auto_properties: Default::default(), + space_view_state: space_view_class_registry .get_class_or_log_error(space_view_class) - .new_state() + .new_state(), }) - .as_mut() } + + pub fn space_view_props(&self, space_view_id: SpaceViewId) -> &EntityPropertyMap { + self.space_view_states + .get(&space_view_id) + .map_or(&DEFAULT_PROPS, |state| &state.auto_properties) + } +} + +// We delay any modifications to the tree until the end of the frame, +// so that we don't iterate over something while modifying it. +#[derive(Clone, Default)] +pub struct TreeActions { + pub create: Vec, + pub focus_tab: Option, + pub remove: Vec, } // ---------------------------------------------------------------------------- /// Defines the layout of the Viewport pub struct Viewport<'a, 'b> { - /// The initial state of the Viewport read from the blueprint store on this frame. - /// - /// This is used to compare to the possibly mutated blueprint to - /// determine whether or not we need to save changes back - /// to the store as part of `sync_blueprint_changes`. - start_of_frame_snapshot: ViewportBlueprint<'a>, - - // This is what me mutate during the frame. - pub blueprint: ViewportBlueprint<'a>, + /// The blueprint that drives this viewport. This is the source of truth from the store + /// for this frame. + pub blueprint: &'a ViewportBlueprint, + /// The persistent state of the viewport that is not saved to the store but otherwise + /// persis frame-to-frame. pub state: &'b mut ViewportState, + + /// The [`egui_tiles::Tree`] tree that actually manages blueprint layout. This tree needs + /// to be mutable for things like drag-and-drop and is ultimately saved back to the store. + /// at the end of the frame if edited. + pub tree: egui_tiles::Tree, + + /// Actions to perform at the end of the frame. + /// + /// We delay any modifications to the tree until the end of the frame, + /// so that we don't mutate something while inspecitng it. + //TODO(jleibs): Can we use the SystemCommandSender for this, too? + pub deferred_tree_actions: TreeActions, } impl<'a, 'b> Viewport<'a, 'b> { - pub fn from_db(blueprint_db: &'a re_data_store::StoreDb, state: &'b mut ViewportState) -> Self { + pub fn new( + blueprint: &'a ViewportBlueprint, + state: &'b mut ViewportState, + space_view_class_registry: &SpaceViewClassRegistry, + ) -> Self { re_tracing::profile_function!(); - let blueprint = load_viewport_blueprint(blueprint_db); - - let start_of_frame_snapshot = blueprint.clone(); + // If the blueprint tree is empty/missing we need to auto-layout. + let tree = if blueprint.tree.is_empty() && !blueprint.space_views.is_empty() { + super::auto_layout::tree_from_space_views( + space_view_class_registry, + &blueprint.space_views, + ) + } else { + blueprint.tree.clone() + }; Self { - start_of_frame_snapshot, blueprint, state, + tree, + deferred_tree_actions: Default::default(), } } - pub fn sync_blueprint_changes(&self, command_sender: &CommandSender) { - ViewportBlueprint::sync_viewport_blueprint( - &self.start_of_frame_snapshot, - &self.blueprint, - command_sender, - ); - } - pub fn show_add_remove_entities_window(&mut self, space_view_id: SpaceViewId) { self.state.space_view_entity_window = Some(SpaceViewEntityPicker { space_view_id }); } @@ -105,7 +142,7 @@ impl<'a, 'b> Viewport<'a, 'b> { } = self; if let Some(window) = &mut state.space_view_entity_window { - if let Some(space_view) = blueprint.space_views.get_mut(&window.space_view_id) { + if let Some(space_view) = blueprint.space_views.get(&window.space_view_id) { if !window.ui(ctx, ui, space_view) { state.space_view_entity_window = None; } @@ -120,12 +157,14 @@ impl<'a, 'b> Viewport<'a, 'b> { return; } + let mut maximized = blueprint.maximized; + if let Some(space_view_id) = blueprint.maximized { if !blueprint.space_views.contains_key(&space_view_id) { - blueprint.maximized = None; // protect against bad deserialized data + maximized = None; } else if let Some(tile_id) = blueprint.tree.tiles.find_pane(&space_view_id) { if !blueprint.tree.tiles.is_visible(tile_id) { - blueprint.maximized = None; // Automatically de-maximize views that aren't visible anymore. + maximized = None; } } } @@ -138,13 +177,7 @@ impl<'a, 'b> Viewport<'a, 'b> { maximized_tree = egui_tiles::Tree::new("viewport_tree", root, tiles); &mut maximized_tree } else { - if blueprint.tree.is_empty() { - blueprint.tree = super::auto_layout::tree_from_space_views( - ctx.space_view_class_registry, - &blueprint.space_views, - ); - } - &mut blueprint.tree + &mut self.tree }; let executed_systems_per_space_view = execute_systems_for_space_views( @@ -162,7 +195,7 @@ impl<'a, 'b> Viewport<'a, 'b> { viewport_state: state, ctx, space_views: &blueprint.space_views, - maximized: &mut blueprint.maximized, + maximized: &mut maximized, edited: false, space_views_displayed_current_frame: Vec::new(), executed_systems_per_space_view, @@ -180,41 +213,62 @@ impl<'a, 'b> Viewport<'a, 'b> { ); } - blueprint.auto_layout = false; + blueprint.set_auto_layout(false, ctx); } state.space_views_displayed_last_frame = tab_viewer.space_views_displayed_current_frame; }); + + self.blueprint.set_maximized(maximized, ctx); } pub fn on_frame_start(&mut self, ctx: &ViewerContext<'_>, spaces_info: &SpaceInfoCollection) { re_tracing::profile_function!(); - for space_view in self.blueprint.space_views.values_mut() { - let space_view_state = self.state.space_view_state_mut( + for space_view in self.blueprint.space_views.values() { + let PerSpaceViewState { + auto_properties, + space_view_state, + } = self.state.space_view_state_mut( ctx.space_view_class_registry, space_view.id, space_view.class_identifier(), ); - space_view.on_frame_start(ctx, space_view_state); + space_view.on_frame_start(ctx, space_view_state.as_mut(), auto_properties); } if self.blueprint.auto_space_views { + let mut new_space_views = vec![]; for space_view_candidate in default_created_space_views(ctx, spaces_info, ctx.entities_per_system_per_class) { - if self.should_auto_add_space_view(&space_view_candidate) { - self.blueprint.add_space_view(space_view_candidate); + if self.should_auto_add_space_view(&new_space_views, &space_view_candidate) { + new_space_views.push(space_view_candidate); } } + + self.blueprint.add_space_views( + new_space_views.into_iter(), + ctx, + &mut self.deferred_tree_actions, + ); } } - fn should_auto_add_space_view(&self, space_view_candidate: &SpaceViewBlueprint) -> bool { + fn should_auto_add_space_view( + &self, + already_added: &[SpaceViewBlueprint], + space_view_candidate: &SpaceViewBlueprint, + ) -> bool { re_tracing::profile_function!(); - for existing_view in self.blueprint.space_views.values() { + for existing_view in self + .blueprint + .space_views + .values() + .chain(already_added.iter()) + { if existing_view.space_origin == space_view_candidate.space_origin { if existing_view.entities_determined_by_user { // Since the user edited a space view with the same space path, we can't be sure our new one isn't redundant. @@ -236,6 +290,79 @@ impl<'a, 'b> Viewport<'a, 'b> { true } + /// Process any deferred `TreeActions` and then sync to blueprint + pub fn update_and_sync_tile_tree_to_blueprint(&mut self, ctx: &ViewerContext<'_>) { + // At the end of the Tree-UI, we can safely apply deferred actions. + + let mut reset = false; + + let TreeActions { + create, + mut focus_tab, + remove, + } = std::mem::take(&mut self.deferred_tree_actions); + + for space_view in &create { + if self.blueprint.auto_layout { + // Re-run the auto-layout next frame: + re_log::trace!( + "Added a space view with no user edits yet - will re-run auto-layout" + ); + + reset = true; + } else if let Some(root_id) = self.tree.root { + let tile_id = self.tree.tiles.insert_pane(*space_view); + if let Some(egui_tiles::Tile::Container(container)) = + self.tree.tiles.get_mut(root_id) + { + re_log::trace!("Inserting new space view into root container"); + container.add_child(tile_id); + } else { + re_log::trace!("Root was not a container - will re-run auto-layout"); + reset = true; + } + } else { + re_log::trace!("No root found - will re-run auto-layout"); + } + + focus_tab = Some(*space_view); + } + + if let Some(focus_tab) = &focus_tab { + let found = self.tree.make_active(|tile| match tile { + egui_tiles::Tile::Pane(space_view_id) => space_view_id == focus_tab, + egui_tiles::Tile::Container(_) => false, + }); + re_log::trace!("Found tab {focus_tab}: {found}"); + } + + for tile_id in remove { + for tile in self.tree.tiles.remove_recursively(tile_id) { + re_log::trace!("Removing tile {tile_id:?}"); + if let egui_tiles::Tile::Pane(space_view_id) = tile { + re_log::trace!("Removing space-view {space_view_id}"); + self.tree.tiles.remove(tile_id); + self.blueprint.remove_space_view(&space_view_id, ctx); + } + } + + if Some(tile_id) == self.tree.root { + self.tree.root = None; + } + } + + if reset { + // We don't run auto-layout here since the new space views also haven't been + // written to the store yet. + re_log::trace!("Clearing the blueprint tree to force reset on the next frame"); + self.tree = egui_tiles::Tree::empty("viewport_tree"); + } + + // Finally, save any edits to the blueprint tree + // This is a no-op if the tree hasn't changed. + self.blueprint.set_tree(&self.tree, ctx); + } + /// If `false`, the item is referring to data that is not present in this blueprint. #[inline] pub fn is_item_valid(&self, item: &Item) -> bool { @@ -306,7 +433,10 @@ impl<'a, 'b> egui_tiles::Behavior for TabViewer<'a, 'b> { space_view_blueprint.execute_systems(self.ctx, latest_at, highlights) }; - let space_view_state = self.viewport_state.space_view_state_mut( + let PerSpaceViewState { + auto_properties: _, + space_view_state, + } = self.viewport_state.space_view_state_mut( self.ctx.space_view_class_registry, space_view_blueprint.id, space_view_blueprint.class_identifier(), @@ -315,7 +445,13 @@ impl<'a, 'b> egui_tiles::Behavior for TabViewer<'a, 'b> { self.space_views_displayed_current_frame .push(space_view_blueprint.id); - space_view_blueprint.scene_ui(space_view_state, self.ctx, ui, &query, system_output); + space_view_blueprint.scene_ui( + space_view_state.as_mut(), + self.ctx, + ui, + &query, + system_output, + ); Default::default() } diff --git a/crates/re_viewport/src/viewport_blueprint.rs b/crates/re_viewport/src/viewport_blueprint.rs index 16847cdcff89..165f3517e662 100644 --- a/crates/re_viewport/src/viewport_blueprint.rs +++ b/crates/re_viewport/src/viewport_blueprint.rs @@ -1,41 +1,24 @@ use std::collections::BTreeMap; use re_arrow_store::LatestAtQuery; -use re_data_store::{EntityPath, StoreDb}; -use re_log_types::{DataRow, RowId, TimePoint, Timeline}; +use re_data_store::EntityPath; +use re_log_types::Timeline; use re_query::query_archetype; -use re_types_core::{archetypes::Clear, AsComponents as _}; -use re_viewer_context::{ - CommandSender, Item, SpaceViewClassIdentifier, SpaceViewId, SystemCommand, SystemCommandSender, - ViewerContext, -}; +use re_viewer_context::{Item, SpaceViewClassIdentifier, SpaceViewId, ViewerContext}; use crate::{ blueprint::components::{ AutoLayout, AutoSpaceViews, IncludedSpaceViews, SpaceViewMaximized, ViewportLayout, }, - space_info::SpaceInfoCollection, space_view::SpaceViewBlueprint, - space_view_heuristics::default_created_space_views, + viewport::TreeActions, VIEWPORT_PATH, }; // ---------------------------------------------------------------------------- -// We delay any modifications to the tree until the end of the frame, -// so that we don't iterate over something while modifying it. -#[derive(Clone, Default)] -pub(crate) struct TreeActions { - pub focus_tab: Option, - pub remove: Vec, -} - /// Describes the layout and contents of the Viewport Panel. -#[derive(Clone)] -pub struct ViewportBlueprint<'a> { - /// The StoreDb used to instantiate this blueprint - blueprint_db: &'a StoreDb, - +pub struct ViewportBlueprint { /// Where the space views are stored. /// /// Not a hashmap in order to preserve the order of the space views. @@ -54,15 +37,98 @@ pub struct ViewportBlueprint<'a> { /// Whether or not space views should be created automatically. pub auto_space_views: bool, - - /// Actions to perform at the end of the frame. - /// - /// We delay any modifications to the tree until the end of the frame, - /// so that we don't mutate something while inspecitng it. - pub(crate) deferred_tree_actions: TreeActions, } -impl<'a> ViewportBlueprint<'a> { +impl ViewportBlueprint { + pub fn try_from_db(blueprint_db: &re_data_store::StoreDb) -> Self { + re_tracing::profile_function!(); + + let query = LatestAtQuery::latest(Timeline::default()); + + let arch = match query_archetype::( + blueprint_db.store(), + &query, + &VIEWPORT_PATH.into(), + ) + .and_then(|arch| arch.to_archetype()) + { + Ok(arch) => arch, + Err(re_query::QueryError::PrimaryNotFound(_)) => { + // Empty Store + Default::default() + } + Err(err) => { + if cfg!(debug_assertions) { + re_log::error!("Failed to load viewport blueprint: {err}."); + } else { + re_log::debug!("Failed to load viewport blueprint: {err}."); + } + Default::default() + } + }; + + let space_view_ids: Vec = + arch.space_views.0.iter().map(|id| (*id).into()).collect(); + + let space_views: BTreeMap = space_view_ids + .into_iter() + .filter_map(|space_view: SpaceViewId| { + SpaceViewBlueprint::try_from_db(space_view, blueprint_db) + }) + .map(|sv| (sv.id, sv)) + .collect(); + + let auto_layout = arch.auto_layout.unwrap_or_default().0; + + let auto_space_views = arch.auto_space_views.map_or_else( + || { + // Only enable auto-space-views if this is the app-default blueprint + blueprint_db + .store_info() + .map_or(false, |ri| ri.is_app_default_blueprint()) + }, + |auto| auto.0, + ); + + let maximized = arch.maximized.and_then(|id| id.0.map(|id| id.into())); + + let tree = blueprint_db + .store() + .query_timeless_component_quiet::(&VIEWPORT_PATH.into()) + .map(|space_view| space_view.value) + .unwrap_or_default() + .0; + + ViewportBlueprint { + space_views, + tree, + maximized, + auto_layout, + auto_space_views, + } + + // TODO(jleibs): Need to figure out if we have to re-enable support for + // auto-discovery of SpaceViews logged via the experimental blueprint APIs. + /* + let unknown_space_views: HashMap<_, _> = space_views + .iter() + .filter(|(k, _)| !viewport_layout.space_view_keys.contains(k)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + */ + + // 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. + // TODO(jleibs): what to do about auto-discovery? + /* + for (_, view) in unknown_space_views { + viewport.add_space_view(view); + } + */ + } + /// Determine whether all views in a blueprint are invalid. /// /// This most commonly happens due to a change in struct definition that @@ -80,41 +146,6 @@ impl<'a> ViewportBlueprint<'a> { .all(|sv| sv.class_identifier() == &SpaceViewClassIdentifier::invalid()) } - /// Reset the blueprint to a default state using some heuristics. - pub fn reset(&mut self, ctx: &ViewerContext<'_>, spaces_info: &SpaceInfoCollection) { - // TODO(jleibs): When using blueprint API, "reset" should go back to the initially transmitted - // blueprint, not the default blueprint. - re_tracing::profile_function!(); - - let ViewportBlueprint { - blueprint_db: _, - space_views, - tree, - maximized, - auto_layout, - auto_space_views, - deferred_tree_actions: tree_actions, - } = self; - - // Note, it's important that these values match the behavior in `load_viewport_blueprint` below. - *space_views = Default::default(); - *tree = egui_tiles::Tree::empty("viewport_tree"); - *maximized = None; - *auto_layout = true; - // Only enable auto-space-views if this is the app-default blueprint - *auto_space_views = self - .blueprint_db - .store_info() - .map_or(false, |ri| ri.is_app_default_blueprint()); - *tree_actions = Default::default(); - - for space_view in - default_created_space_views(ctx, spaces_info, ctx.entities_per_system_per_class) - { - self.add_space_view(space_view); - } - } - pub fn space_view_ids(&self) -> impl Iterator + '_ { self.space_views.keys() } @@ -130,28 +161,28 @@ impl<'a> ViewportBlueprint<'a> { self.space_views.get_mut(space_view_id) } - pub(crate) fn remove(&mut self, space_view_id: &SpaceViewId) -> Option { - self.mark_user_interaction(); - - let Self { - blueprint_db: _, - space_views, - tree, - maximized, - auto_layout: _, - auto_space_views: _, - deferred_tree_actions: _, - } = self; + pub(crate) fn remove_space_view(&self, space_view_id: &SpaceViewId, ctx: &ViewerContext<'_>) { + self.mark_user_interaction(ctx); - if *maximized == Some(*space_view_id) { - *maximized = None; + // Remove the space view from the store + if let Some(space_view) = self.space_views.get(space_view_id) { + space_view.clear(ctx); } - if let Some(tile_id) = tree.tiles.find_pane(space_view_id) { - tree.tiles.remove(tile_id); + // If the space-view was maximized, clean it up + if self.maximized == Some(*space_view_id) { + self.set_maximized(None, ctx); } - space_views.remove(space_view_id) + // Filter the space-view from the included space-views + let component = IncludedSpaceViews( + self.space_views + .keys() + .filter(|id| id != &space_view_id) + .map(|id| (*id).into()) + .collect(), + ); + ctx.save_blueprint_component(&VIEWPORT_PATH.into(), component); } /// If `false`, the item is referring to data that is not present in this blueprint. @@ -184,62 +215,68 @@ impl<'a> ViewportBlueprint<'a> { } } - pub fn mark_user_interaction(&mut self) { + pub fn mark_user_interaction(&self, ctx: &ViewerContext<'_>) { if self.auto_layout { re_log::trace!("User edits - will no longer auto-layout"); } - self.auto_layout = false; - self.auto_space_views = false; + self.set_auto_layout(false, ctx); + self.set_auto_space_views(false, ctx); } - pub fn add_space_view(&mut self, mut space_view: SpaceViewBlueprint) -> SpaceViewId { - let space_view_id = space_view.id; + /// Add a set of space views to the viewport. + /// + /// NOTE: Calling this more than once per frame will result in lost data. + /// Each call to `add_space_views` emits an updated list of [`IncludedSpaceViews`] + /// Built by taking the list of [`IncludedSpaceViews`] from the current frame + /// and adding the new space views to it. Since this the edit is not applied until + /// the end of frame the second call will see a stale version of the data. + // TODO(jleibs): Better safety check here. + pub fn add_space_views( + &self, + space_views: impl Iterator, + ctx: &ViewerContext<'_>, + tree_actions: &mut TreeActions, + ) { + let mut new_ids: Vec<_> = vec![]; + + for mut space_view in space_views { + let space_view_id = space_view.id; - // Find a unique name for the space view - let mut candidate_name = space_view.display_name.clone(); - let mut append_count = 1; - let unique_name = 'outer: loop { - for view in &self.space_views { - if candidate_name == view.1.display_name { - append_count += 1; - candidate_name = format!("{} ({})", space_view.display_name, append_count); + // Find a unique name for the space view + let mut candidate_name = space_view.display_name.clone(); + let mut append_count = 1; + let unique_name = 'outer: loop { + for view in &self.space_views { + if candidate_name == view.1.display_name { + append_count += 1; + candidate_name = format!("{} ({})", space_view.display_name, append_count); - continue 'outer; + continue 'outer; + } } - } - break candidate_name; - }; + break candidate_name; + }; - space_view.display_name = unique_name; + space_view.display_name = unique_name; - self.space_views.insert(space_view_id, space_view); + // Save the space view to the store + space_view.save_to_blueprint_store(ctx); - if self.auto_layout { - // Re-run the auto-layout next frame: - re_log::trace!("Added a space view with no user edits yet - will re-run auto-layout"); - self.tree = egui_tiles::Tree::empty("viewport_tree"); - } else { - // Try to insert it in the tree, in the top level: - if let Some(root_id) = self.tree.root { - let tile_id = self.tree.tiles.insert_pane(space_view_id); - if let Some(egui_tiles::Tile::Container(container)) = - self.tree.tiles.get_mut(root_id) - { - re_log::trace!("Inserting new space view into root container"); - container.add_child(tile_id); - } else { - re_log::trace!("Root was not a container - will re-run auto-layout"); - self.tree = egui_tiles::Tree::empty("viewport_tree"); - } - } else { - re_log::trace!("No root found - will re-run auto-layout"); - } + // Update the space-view ids: + new_ids.push(space_view_id); } - self.deferred_tree_actions.focus_tab = Some(space_view_id); + if !new_ids.is_empty() { + tree_actions.create.extend(new_ids.iter()); + + let updated_ids: Vec<_> = self.space_views.keys().chain(new_ids.iter()).collect(); - space_view_id + let component = + IncludedSpaceViews(updated_ids.into_iter().map(|id| (*id).into()).collect()); + + ctx.save_blueprint_component(&VIEWPORT_PATH.into(), component); + } } #[allow(clippy::unused_self)] @@ -265,248 +302,36 @@ impl<'a> ViewportBlueprint<'a> { .collect() } - /// Compares the before and after snapshots and sends any necessary deltas to the store. - pub fn sync_viewport_blueprint( - before: &ViewportBlueprint<'_>, - after: &ViewportBlueprint<'_>, - command_sender: &CommandSender, - ) { - let mut deltas = vec![]; - - let entity_path = EntityPath::from(VIEWPORT_PATH); - - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - - if after.space_views.len() != before.space_views.len() - || after - .space_views - .keys() - .zip(before.space_views.keys()) - .any(|(a, b)| a != b) - { - let component = - IncludedSpaceViews(after.space_views.keys().map(|id| (*id).into()).collect()); - add_delta_from_single_component(&mut deltas, &entity_path, &timepoint, component); - } - - if after.auto_layout != before.auto_layout { - let component = AutoLayout(after.auto_layout); - add_delta_from_single_component(&mut deltas, &entity_path, &timepoint, component); - } - - if after.auto_space_views != before.auto_space_views { - let component = AutoSpaceViews(after.auto_space_views); - add_delta_from_single_component(&mut deltas, &entity_path, &timepoint, component); - } - - if after.maximized != before.maximized { - let component = SpaceViewMaximized(after.maximized.map(|id| id.into())); - add_delta_from_single_component(&mut deltas, &entity_path, &timepoint, component); - } - - if after.tree != before.tree { - re_log::trace!("Syncing tree"); - - let component = ViewportLayout(after.tree.clone()); - - add_delta_from_single_component(&mut deltas, &entity_path, &timepoint, component); - } - - // Add any new or modified space views - for id in after.space_view_ids() { - if let Some(space_view) = after.space_view(id) { - sync_space_view(&mut deltas, space_view, before.space_view(id)); - } + #[inline] + pub fn set_auto_layout(&self, value: bool, ctx: &ViewerContext<'_>) { + if self.auto_layout != value { + let component = AutoLayout(value); + ctx.save_blueprint_component(&VIEWPORT_PATH.into(), component); } - - // Remove any deleted space views - for space_view_id in before.space_view_ids() { - if after.space_view(space_view_id).is_none() { - clear_space_view(&mut deltas, space_view_id); - } - } - - command_sender.send_system(SystemCommand::UpdateBlueprint( - after.blueprint_db.store_id().clone(), - deltas, - )); } -} - -// ---------------------------------------------------------------------------- -// TODO(jleibs): Move this helper to a better location -fn add_delta_from_single_component<'a, C>( - deltas: &mut Vec, - entity_path: &EntityPath, - timepoint: &TimePoint, - component: C, -) where - C: re_types::Component + Clone + 'a, - std::borrow::Cow<'a, C>: std::convert::From, -{ - let row = DataRow::from_cells1_sized( - RowId::new(), - entity_path.clone(), - timepoint.clone(), - 1, - [component], - ) - .unwrap(); // TODO(emilk): statically check that the component is a mono-component - then this cannot fail! - - deltas.push(row); -} - -// ---------------------------------------------------------------------------- - -pub fn load_viewport_blueprint(blueprint_db: &re_data_store::StoreDb) -> ViewportBlueprint<'_> { - re_tracing::profile_function!(); - - let query = LatestAtQuery::latest(Timeline::default()); - - let arch = match query_archetype::( - blueprint_db.store(), - &query, - &VIEWPORT_PATH.into(), - ) - .and_then(|arch| arch.to_archetype()) - { - Ok(arch) => arch, - Err(re_query::QueryError::PrimaryNotFound(_)) => { - // Empty Store - Default::default() + #[inline] + pub fn set_auto_space_views(&self, value: bool, ctx: &ViewerContext<'_>) { + if self.auto_layout != value { + let component = AutoSpaceViews(value); + ctx.save_blueprint_component(&VIEWPORT_PATH.into(), component); } - Err(err) => { - if cfg!(debug_assertions) { - re_log::error!("Failed to load viewport blueprint: {err}."); - } else { - re_log::debug!("Failed to load viewport blueprint: {err}."); - } - Default::default() - } - }; - - let space_view_ids: Vec = - arch.space_views.0.iter().map(|id| (*id).into()).collect(); - - let space_views: BTreeMap = space_view_ids - .into_iter() - .filter_map(|space_view: SpaceViewId| { - SpaceViewBlueprint::try_from_db(&space_view.as_entity_path(), blueprint_db) - }) - .map(|sv| (sv.id, sv)) - .collect(); - - let auto_layout = arch.auto_layout.unwrap_or_default().0; - - let auto_space_views = arch.auto_space_views.map_or_else( - || { - // Only enable auto-space-views if this is the app-default blueprint - blueprint_db - .store_info() - .map_or(false, |ri| ri.is_app_default_blueprint()) - }, - |auto| auto.0, - ); - - let maximized = arch.maximized.and_then(|id| id.0.map(|id| id.into())); - - let tree = blueprint_db - .store() - .query_timeless_component_quiet::(&VIEWPORT_PATH.into()) - .map(|space_view| space_view.value) - .unwrap_or_default() - .0; - - ViewportBlueprint { - blueprint_db, - space_views, - tree, - maximized, - auto_layout, - auto_space_views, - deferred_tree_actions: Default::default(), } - // TODO(jleibs): Need to figure out if we have to re-enable support for - // auto-discovery of SpaceViews logged via the experimental blueprint APIs. - /* - let unknown_space_views: HashMap<_, _> = space_views - .iter() - .filter(|(k, _)| !viewport_layout.space_view_keys.contains(k)) - .map(|(k, v)| (*k, v.clone())) - .collect(); - */ - - // 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. - // TODO(jleibs): what to do about auto-discovery? - /* - for (_, view) in unknown_space_views { - viewport.add_space_view(view); - } - */ -} - -// ---------------------------------------------------------------------------- - -pub fn sync_space_view( - deltas: &mut Vec, - space_view: &SpaceViewBlueprint, - snapshot: Option<&SpaceViewBlueprint>, -) { - if snapshot.map_or(true, |snapshot| space_view.has_edits(snapshot)) { - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - - let arch = re_types::blueprint::archetypes::SpaceViewBlueprint::new( - space_view.class_identifier().as_str(), - ) - .with_display_name(space_view.display_name.clone()) - .with_space_origin(&space_view.space_origin) - .with_entities_determined_by_user(space_view.entities_determined_by_user) - .with_contents(space_view.queries.iter().map(|q| q.id)); - - if let Ok(row) = DataRow::from_archetype( - RowId::new(), - timepoint.clone(), - space_view.entity_path(), - &arch, - ) { - deltas.push(row); - } - - // The only time we need to create a query is if this is a new space-view. All other edits - // happen directly via `UpdateBlueprint` commands. - if snapshot.is_none() { - for query in &space_view.queries { - add_delta_from_single_component( - deltas, - &query.id.as_entity_path(), - &timepoint, - query.expressions.clone(), - ); - } + #[inline] + pub fn set_maximized(&self, space_view_id: Option, ctx: &ViewerContext<'_>) { + if self.maximized != space_view_id { + let component = SpaceViewMaximized(space_view_id.map(|id| id.into())); + ctx.save_blueprint_component(&VIEWPORT_PATH.into(), component); } } -} - -pub fn clear_space_view(deltas: &mut Vec, space_view_id: &SpaceViewId) { - // TODO(jleibs): Seq instead of timeless? - let timepoint = TimePoint::timeless(); - if let Ok(row) = DataRow::from_component_batches( - RowId::new(), - timepoint, - space_view_id.as_entity_path(), - Clear::recursive() - .as_component_batches() - .iter() - .map(|b| b.as_ref()), - ) { - deltas.push(row); + #[inline] + pub fn set_tree(&self, tree: &egui_tiles::Tree, ctx: &ViewerContext<'_>) { + if &self.tree != tree { + re_log::trace!("Updating the layout tree"); + let component = ViewportLayout(tree.clone()); + ctx.save_blueprint_component(&VIEWPORT_PATH.into(), component); + } } } diff --git a/crates/re_viewport/src/viewport_blueprint_ui.rs b/crates/re_viewport/src/viewport_blueprint_ui.rs index b57e89671b91..fe381759438f 100644 --- a/crates/re_viewport/src/viewport_blueprint_ui.rs +++ b/crates/re_viewport/src/viewport_blueprint_ui.rs @@ -13,11 +13,11 @@ use re_viewer_context::{ }; use crate::{ - space_view_heuristics::all_possible_space_views, viewport_blueprint::TreeActions, - SpaceInfoCollection, SpaceViewBlueprint, ViewportBlueprint, + space_view_heuristics::all_possible_space_views, SpaceInfoCollection, SpaceViewBlueprint, + Viewport, }; -impl ViewportBlueprint<'_> { +impl Viewport<'_, '_> { /// Show the blueprint panel tree view. pub fn tree_ui(&mut self, ctx: &ViewerContext<'_>, ui: &mut egui::Ui) { re_tracing::profile_function!(); @@ -32,28 +32,6 @@ impl ViewportBlueprint<'_> { } }); }); - - let TreeActions { focus_tab, remove } = std::mem::take(&mut self.deferred_tree_actions); - - if let Some(focus_tab) = &focus_tab { - let found = self.tree.make_active(|tile| match tile { - egui_tiles::Tile::Pane(space_view_id) => space_view_id == focus_tab, - egui_tiles::Tile::Container(_) => false, - }); - re_log::trace!("Found tab {focus_tab}: {found}"); - } - - for tile_id in remove { - for tile in self.tree.tiles.remove_recursively(tile_id) { - if let egui_tiles::Tile::Pane(space_view_id) = tile { - self.remove(&space_view_id); - } - } - - if Some(tile_id) == self.tree.root { - self.tree.root = None; - } - } } /// If a group or spaceview has a total of this number of elements, show its subtree by default? @@ -127,16 +105,18 @@ impl ViewportBlueprint<'_> { item_ui::select_hovered_on_click(ctx, &response, &[item]); if remove { - self.mark_user_interaction(); + self.blueprint.mark_user_interaction(ctx); self.deferred_tree_actions.remove.push(tile_id); } if visibility_changed { - if self.auto_layout { + if self.blueprint.auto_layout { re_log::trace!("Container visibility changed - will no longer auto-layout"); } - self.auto_layout = false; // Keep `auto_space_views` enabled. + // Keep `auto_space_views` enabled. + self.blueprint.set_auto_layout(false, ctx); + self.tree.set_visible(tile_id, visible); } } @@ -148,7 +128,7 @@ impl ViewportBlueprint<'_> { tile_id: egui_tiles::TileId, space_view_id: &SpaceViewId, ) { - let Some(space_view) = self.space_views.get_mut(space_view_id) else { + let Some(space_view) = self.blueprint.space_views.get(space_view_id) else { re_log::warn_once!("Bug: asked to show a ui for a Space View that doesn't exist"); self.deferred_tree_actions.remove.push(tile_id); return; @@ -217,11 +197,13 @@ impl ViewportBlueprint<'_> { item_ui::select_hovered_on_click(ctx, &response, &[item]); if visibility_changed { - if self.auto_layout { + if self.blueprint.auto_layout { re_log::trace!("Space view visibility changed - will no longer auto-layout"); } - self.auto_layout = false; // Keep `auto_space_views` enabled. + // Keep `auto_space_views` enabled. + self.blueprint.set_auto_layout(false, ctx); + self.tree.set_visible(tile_id, visible); } } @@ -231,7 +213,7 @@ impl ViewportBlueprint<'_> { ui: &mut egui::Ui, query_result: &DataQueryResult, result_handle: DataResultHandle, - space_view: &mut SpaceViewBlueprint, + space_view: &SpaceViewBlueprint, space_view_visible: bool, ) { let Some(top_node) = query_result.tree.lookup_node(result_handle) else { @@ -425,8 +407,12 @@ impl ViewportBlueprint<'_> { .clicked() { ui.close_menu(); - let new_space_view_id = self.add_space_view(space_view); - ctx.set_single_selection(&Item::SpaceView(new_space_view_id)); + ctx.set_single_selection(&Item::SpaceView(space_view.id)); + self.blueprint.add_space_views( + std::iter::once(space_view), + ctx, + &mut self.deferred_tree_actions, + ); } }; diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.cpp b/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.cpp index db058f0dfecb..9a967352ce3f 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.cpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.cpp @@ -21,8 +21,8 @@ namespace rerun { RR_RETURN_NOT_OK(result.error); cells.push_back(std::move(result.value)); } - { - auto result = DataCell::from_loggable(archetype.layout); + if (archetype.layout.has_value()) { + auto result = DataCell::from_loggable(archetype.layout.value()); RR_RETURN_NOT_OK(result.error); cells.push_back(std::move(result.value)); } diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.hpp index 6be8d85b29b2..10600134f7bf 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes/viewport_blueprint.hpp @@ -26,7 +26,7 @@ namespace rerun::blueprint::archetypes { rerun::blueprint::components::IncludedSpaceViews space_views; /// The layout of the space-views - rerun::blueprint::components::ViewportLayout layout; + std::optional layout; /// Show one tab as maximized? std::optional maximized; @@ -50,11 +50,15 @@ namespace rerun::blueprint::archetypes { ViewportBlueprint() = default; ViewportBlueprint(ViewportBlueprint&& other) = default; - explicit ViewportBlueprint( - rerun::blueprint::components::IncludedSpaceViews _space_views, - rerun::blueprint::components::ViewportLayout _layout - ) - : space_views(std::move(_space_views)), layout(std::move(_layout)) {} + explicit ViewportBlueprint(rerun::blueprint::components::IncludedSpaceViews _space_views) + : space_views(std::move(_space_views)) {} + + /// The layout of the space-views + ViewportBlueprint with_layout(rerun::blueprint::components::ViewportLayout _layout) && { + layout = std::move(_layout); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } /// Show one tab as maximized? ViewportBlueprint with_maximized(rerun::blueprint::components::SpaceViewMaximized _maximized diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/viewport_blueprint.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/viewport_blueprint.py index 82b519243b1e..09237654db12 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/viewport_blueprint.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/viewport_blueprint.py @@ -24,8 +24,8 @@ class ViewportBlueprint(Archetype): def __init__( self: Any, space_views: components.IncludedSpaceViewsLike, - layout: components.ViewportLayoutLike, *, + layout: components.ViewportLayoutLike | None = None, maximized: datatypes.UuidLike | None = None, auto_layout: components.AutoLayoutLike | None = None, auto_space_views: components.AutoSpaceViewsLike | None = None, @@ -86,9 +86,10 @@ def _clear(cls) -> ViewportBlueprint: # # (Docstring intentionally commented out to hide this field from the docs) - layout: components.ViewportLayoutBatch = field( - metadata={"component": "required"}, - converter=components.ViewportLayoutBatch._required, # type: ignore[misc] + layout: components.ViewportLayoutBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.ViewportLayoutBatch._optional, # type: ignore[misc] ) # The layout of the space-views #