diff --git a/Cargo.lock b/Cargo.lock index 24ccac865f04..b50ed20b3230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,7 +1491,7 @@ dependencies = [ [[package]] name = "egui_tiles" version = "0.1.0" -source = "git+https://github.com/rerun-io/egui_tiles.git?rev=f958d8af7ed27d925bc2ff7862fb1c1c45961b9e#f958d8af7ed27d925bc2ff7862fb1c1c45961b9e" +source = "git+https://github.com/rerun-io/egui_tiles.git?rev=3fff2b01310aa3569e29d2dd3ee97b2e62c45870#3fff2b01310aa3569e29d2dd3ee97b2e62c45870" dependencies = [ "ahash 0.8.3", "egui", diff --git a/Cargo.toml b/Cargo.toml index 916d7328113e..6d6f77a36d45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,4 +144,4 @@ debug = true # As a last resport, patch with a commit to our own repository. # ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk. -egui_tiles = { git = "https://github.com/rerun-io/egui_tiles.git", rev = "f958d8af7ed27d925bc2ff7862fb1c1c45961b9e" } +egui_tiles = { git = "https://github.com/rerun-io/egui_tiles.git", rev = "3fff2b01310aa3569e29d2dd3ee97b2e62c45870" } # 2023-06-29: drag-and-drop fixes diff --git a/crates/re_space_view_bar_chart/src/space_view_class.rs b/crates/re_space_view_bar_chart/src/space_view_class.rs index a4f6db8c40b8..cc49f5971a9e 100644 --- a/crates/re_space_view_bar_chart/src/space_view_class.rs +++ b/crates/re_space_view_bar_chart/src/space_view_class.rs @@ -50,6 +50,10 @@ impl SpaceViewClass for BarChartSpaceView { None } + fn layout_priority(&self) -> re_viewer_context::SpaceViewClassLayoutPriority { + re_viewer_context::SpaceViewClassLayoutPriority::Low + } + fn selection_ui( &self, _ctx: &mut ViewerContext<'_>, diff --git a/crates/re_space_view_spatial/src/space_view_class.rs b/crates/re_space_view_spatial/src/space_view_class.rs index f25f59daf412..636f1173637b 100644 --- a/crates/re_space_view_spatial/src/space_view_class.rs +++ b/crates/re_space_view_spatial/src/space_view_class.rs @@ -38,6 +38,10 @@ impl SpaceViewClass for SpatialSpaceView { } } + fn layout_priority(&self) -> re_viewer_context::SpaceViewClassLayoutPriority { + re_viewer_context::SpaceViewClassLayoutPriority::High + } + fn prepare_populate( &self, ctx: &mut re_viewer_context::ViewerContext<'_>, diff --git a/crates/re_space_view_tensor/src/space_view_class.rs b/crates/re_space_view_tensor/src/space_view_class.rs index 80cb922fdd7f..162eca9bc38b 100644 --- a/crates/re_space_view_tensor/src/space_view_class.rs +++ b/crates/re_space_view_tensor/src/space_view_class.rs @@ -139,6 +139,10 @@ impl SpaceViewClass for TensorSpaceView { None } + fn layout_priority(&self) -> re_viewer_context::SpaceViewClassLayoutPriority { + re_viewer_context::SpaceViewClassLayoutPriority::Medium + } + fn selection_ui( &self, ctx: &mut ViewerContext<'_>, diff --git a/crates/re_space_view_text/src/space_view_class.rs b/crates/re_space_view_text/src/space_view_class.rs index f9c97753202c..f1abf0e7a5d9 100644 --- a/crates/re_space_view_text/src/space_view_class.rs +++ b/crates/re_space_view_text/src/space_view_class.rs @@ -58,6 +58,10 @@ impl SpaceViewClass for TextSpaceView { Some(2.0) // Make text logs wide } + fn layout_priority(&self) -> re_viewer_context::SpaceViewClassLayoutPriority { + re_viewer_context::SpaceViewClassLayoutPriority::Low + } + fn selection_ui( &self, ctx: &mut ViewerContext<'_>, diff --git a/crates/re_space_view_text_box/src/space_view_class.rs b/crates/re_space_view_text_box/src/space_view_class.rs index 57bc008033cd..56612b4d11f1 100644 --- a/crates/re_space_view_text_box/src/space_view_class.rs +++ b/crates/re_space_view_text_box/src/space_view_class.rs @@ -53,6 +53,10 @@ impl SpaceViewClass for TextBoxSpaceView { "Displays text from a text entry components.".into() } + fn layout_priority(&self) -> re_viewer_context::SpaceViewClassLayoutPriority { + re_viewer_context::SpaceViewClassLayoutPriority::Low + } + fn selection_ui( &self, ctx: &mut ViewerContext<'_>, diff --git a/crates/re_space_view_time_series/src/space_view_class.rs b/crates/re_space_view_time_series/src/space_view_class.rs index e96369136367..7e1f79509abc 100644 --- a/crates/re_space_view_time_series/src/space_view_class.rs +++ b/crates/re_space_view_time_series/src/space_view_class.rs @@ -59,6 +59,10 @@ impl SpaceViewClass for TimeSeriesSpaceView { None } + fn layout_priority(&self) -> re_viewer_context::SpaceViewClassLayoutPriority { + re_viewer_context::SpaceViewClassLayoutPriority::Low + } + fn selection_ui( &self, _ctx: &mut ViewerContext<'_>, diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_viewer/src/ui/selection_panel.rs index cbf5e559de39..1bdba66580c2 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_viewer/src/ui/selection_panel.rs @@ -250,13 +250,15 @@ fn blueprint_ui( space_view.class_name(), ); - space_view.class(ctx).selection_ui( - ctx, - ui, - space_view_state, - &space_view.space_origin, - space_view.id, - ); + space_view + .class(ctx.space_view_class_registry) + .selection_ui( + ctx, + ui, + space_view_state, + &space_view.space_origin, + space_view.id, + ); } } diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index a442af02f691..ba55e8d180f7 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -36,9 +36,10 @@ pub use selection_state::{ }; pub use space_view::{ ArchetypeDefinition, DynSpaceViewClass, Scene, SceneContext, SceneContextPart, ScenePart, - ScenePartCollection, SceneQuery, SpaceViewClass, SpaceViewClassName, SpaceViewClassRegistry, - SpaceViewClassRegistryError, SpaceViewEntityHighlight, SpaceViewHighlights, - SpaceViewOutlineMasks, SpaceViewState, TypedScene, + ScenePartCollection, SceneQuery, SpaceViewClass, SpaceViewClassLayoutPriority, + SpaceViewClassName, SpaceViewClassRegistry, SpaceViewClassRegistryError, + SpaceViewEntityHighlight, SpaceViewHighlights, SpaceViewOutlineMasks, SpaceViewState, + TypedScene, }; pub use store_context::StoreContext; pub use tensor::{TensorDecodeCache, TensorStats, TensorStatsCache}; diff --git a/crates/re_viewer_context/src/space_view/dyn_space_view_class.rs b/crates/re_viewer_context/src/space_view/dyn_space_view_class.rs index 1fc86963b0e8..0304573adf7c 100644 --- a/crates/re_viewer_context/src/space_view/dyn_space_view_class.rs +++ b/crates/re_viewer_context/src/space_view/dyn_space_view_class.rs @@ -15,6 +15,21 @@ re_string_interner::declare_new_type!( pub struct SpaceViewClassName; ); +#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq)] +pub enum SpaceViewClassLayoutPriority { + /// This space view can share space with others + /// + /// Used for boring things like text and plots. + Low, + + #[default] + Medium, + + /// Give this space view lots of space. + /// Used for spatial views (2D/3D). + High, +} + /// Defines a class of space view without any concrete types making it suitable for storage and interfacing. /// /// Implemented by [`crate::SpaceViewClass`]. @@ -56,6 +71,9 @@ pub trait DynSpaceViewClass { /// Preferred aspect ratio for the ui tiles of this space view. fn preferred_tile_aspect_ratio(&self, state: &dyn SpaceViewState) -> Option; + /// Controls how likely this space view will get a large tile in the ui. + fn layout_priority(&self) -> SpaceViewClassLayoutPriority; + /// Executed before the scene is populated, can be use for heuristic & state updates before populating the scene. /// /// Is only allowed to access archetypes defined by [`Self::blueprint_archetype`] diff --git a/crates/re_viewer_context/src/space_view/mod.rs b/crates/re_viewer_context/src/space_view/mod.rs index 1994d49bfe88..aa1656554d5b 100644 --- a/crates/re_viewer_context/src/space_view/mod.rs +++ b/crates/re_viewer_context/src/space_view/mod.rs @@ -15,7 +15,8 @@ mod space_view_class_placeholder; mod space_view_class_registry; pub use dyn_space_view_class::{ - ArchetypeDefinition, DynSpaceViewClass, SpaceViewClassName, SpaceViewState, + ArchetypeDefinition, DynSpaceViewClass, SpaceViewClassLayoutPriority, SpaceViewClassName, + SpaceViewState, }; pub use highlights::{SpaceViewEntityHighlight, SpaceViewHighlights, SpaceViewOutlineMasks}; pub use scene::{Scene, TypedScene}; diff --git a/crates/re_viewer_context/src/space_view/space_view_class.rs b/crates/re_viewer_context/src/space_view/space_view_class.rs index c4d0971f2679..8dd1941395c8 100644 --- a/crates/re_viewer_context/src/space_view/space_view_class.rs +++ b/crates/re_viewer_context/src/space_view/space_view_class.rs @@ -48,6 +48,9 @@ pub trait SpaceViewClass: std::marker::Sized { None } + /// Controls how likely this space view will get a large tile in the ui. + fn layout_priority(&self) -> crate::SpaceViewClassLayoutPriority; + /// Optional archetype of the Space View's blueprint properties. /// /// Blueprint components that only apply to the space view itself, not to the entities it displays. @@ -125,6 +128,11 @@ impl DynSpaceViewClass for T { typed_state_wrapper(state, |state| self.preferred_tile_aspect_ratio(state)) } + #[inline] + fn layout_priority(&self) -> crate::SpaceViewClassLayoutPriority { + self.layout_priority() + } + fn blueprint_archetype(&self) -> Option { self.blueprint_archetype() } diff --git a/crates/re_viewer_context/src/space_view/space_view_class_placeholder.rs b/crates/re_viewer_context/src/space_view/space_view_class_placeholder.rs index 3fb6634aca7e..2551a61b2101 100644 --- a/crates/re_viewer_context/src/space_view/space_view_class_placeholder.rs +++ b/crates/re_viewer_context/src/space_view/space_view_class_placeholder.rs @@ -22,6 +22,10 @@ impl SpaceViewClass for SpaceViewClassPlaceholder { "The Space View Class was not recognized.\nThis happens if either the Blueprint specifies an invalid Space View Class or this version of the Viewer does not know about this type.".into() } + fn layout_priority(&self) -> crate::SpaceViewClassLayoutPriority { + crate::SpaceViewClassLayoutPriority::Low + } + fn selection_ui( &self, _ctx: &mut crate::ViewerContext<'_>, diff --git a/crates/re_viewport/src/auto_layout.rs b/crates/re_viewport/src/auto_layout.rs index 352a26c8ce6f..8c37e7fc5d21 100644 --- a/crates/re_viewport/src/auto_layout.rs +++ b/crates/re_viewport/src/auto_layout.rs @@ -1,56 +1,30 @@ //! Code for automatic layout of space views. //! -//! This uses rough heuristics and have a lot of room for improvement. -//! -//! Some of the heuristics include: -//! * We want similar space views together. Similar can mean: -//! * Same category (2D vs text vs …) -//! * Similar entity path (common prefix) -//! * We also want to pick aspect ratios that fit the data pretty well -// TODO(emilk): fix O(N^2) execution time (where N = number of spaces) +//! This uses some very rough heuristics and have a lot of room for improvement. use std::collections::BTreeMap; -use ahash::HashMap; -use egui::Vec2; use itertools::Itertools as _; -use re_data_store::{EntityPath, EntityPathPart}; -use re_viewer_context::{SpaceViewId, ViewerContext}; +use re_viewer_context::SpaceViewId; -use super::{space_view::SpaceViewBlueprint, view_category::ViewCategory}; +use super::space_view::SpaceViewBlueprint; #[derive(Clone, Debug)] -pub struct SpaceMakeInfo { - pub id: SpaceViewId, - - /// Some path we use to group the views by - pub path: EntityPath, - - pub category: ViewCategory, - - /// Desired aspect ratio, if any. - pub aspect_ratio: Option, -} - -enum LayoutSplit { - LeftRight(Box, f32, Box), - TopBottom(Box, f32, Box), - Leaf(SpaceMakeInfo), -} - -enum SplitDirection { - LeftRight { left: Vec2, t: f32, right: Vec2 }, - TopBottom { top: Vec2, t: f32, bottom: Vec2 }, +struct SpaceMakeInfo { + id: SpaceViewId, + layout_priority: re_viewer_context::SpaceViewClassLayoutPriority, } pub(crate) fn tree_from_space_views( - ctx: &mut ViewerContext<'_>, - viewport_size: egui::Vec2, + space_view_class_registry: &re_viewer_context::SpaceViewClassRegistry, space_views: &BTreeMap, - space_view_states: &HashMap>, ) -> egui_tiles::Tree { - let mut space_make_infos = space_views + if space_views.is_empty() { + return egui_tiles::Tree::empty(); + } + + let space_make_infos = space_views .iter() // Sort for determinism: .sorted_by_key(|(space_view_id, space_view)| { @@ -61,233 +35,78 @@ pub(crate) fn tree_from_space_views( ) }) .map(|(space_view_id, space_view)| { - let aspect_ratio = space_view_states.get(space_view_id).and_then(|state| { - space_view - .class(ctx) - .preferred_tile_aspect_ratio(state.as_ref()) - }); - + let layout_priority = space_view + .class(space_view_class_registry) + .layout_priority(); SpaceMakeInfo { id: *space_view_id, - path: space_view.space_origin.clone(), - category: space_view.category, - aspect_ratio, + layout_priority, } }) .collect_vec(); - if space_make_infos.is_empty() { - egui_tiles::Tree::empty() + let mut tiles = egui_tiles::Tiles::default(); + + let root = if space_make_infos.len() == 1 { + tiles.insert_pane(space_make_infos[0].id) + } else if space_make_infos.len() == 3 { + // Special-case for common case that doesn't fit nicely in a grid + arrange_three( + [ + space_make_infos[0].clone(), + space_make_infos[1].clone(), + space_make_infos[2].clone(), + ], + &mut tiles, + ) } else { - let mut tiles = egui_tiles::Tiles::default(); - // Users often organize by path prefix, so we start by splitting along that - let layout = layout_by_path_prefix(viewport_size, &mut space_make_infos); - let root = tree_from_split(&mut tiles, &layout); - egui_tiles::Tree::new(root, tiles) - } + // Arrange it all in a grid that is responsive to changes in viewport size: + let child_tile_ids = space_make_infos + .into_iter() + .map(|smi| tiles.insert_pane(smi.id)) + .collect_vec(); + tiles.insert_grid_tile(child_tile_ids) + }; + + egui_tiles::Tree::new(root, tiles) } -fn tree_from_split( +fn arrange_three( + mut spaces: [SpaceMakeInfo; 3], tiles: &mut egui_tiles::Tiles, - split: &LayoutSplit, ) -> egui_tiles::TileId { - match split { - LayoutSplit::LeftRight(left, fraction, right) => { - let container = egui_tiles::Linear::new_binary( - egui_tiles::LinearDir::Horizontal, - [tree_from_split(tiles, left), tree_from_split(tiles, right)], - *fraction, - ); - tiles.insert_container(container) - } - LayoutSplit::TopBottom(top, fraction, bottom) => { - let container = egui_tiles::Linear::new_binary( - egui_tiles::LinearDir::Vertical, - [tree_from_split(tiles, top), tree_from_split(tiles, bottom)], - *fraction, - ); - tiles.insert_container(container) - } - LayoutSplit::Leaf(space_info) => tiles.insert_pane(space_info.id), - } -} - -/// Group categories together, i.e. so that 2D stuff is next to 2D stuff, and text logs are next to text logs. -fn layout_by_category(viewport_size: egui::Vec2, spaces: &mut [SpaceMakeInfo]) -> LayoutSplit { - assert!(!spaces.is_empty()); - - if spaces.len() == 1 { - LayoutSplit::Leaf(spaces[0].clone()) - } else { - let groups = group_by_category(spaces); - - if groups.len() == 1 { - // All same category - layout_by_path_prefix(viewport_size, spaces) - } else { - // Mixed categories. - split_groups(viewport_size, groups) - } - } -} - -/// Put spaces with common path prefix close together. -fn layout_by_path_prefix(viewport_size: egui::Vec2, spaces: &mut [SpaceMakeInfo]) -> LayoutSplit { - assert!(!spaces.is_empty()); - - if spaces.len() == 1 { - LayoutSplit::Leaf(spaces[0].clone()) - } else { - let groups = group_by_path_prefix(spaces); - - if groups.len() == 1 { - // failed to separate by group - try category instead: - layout_by_category(viewport_size, spaces) - } else { - split_groups(viewport_size, groups) - } - } -} - -fn split_groups(viewport_size: egui::Vec2, groups: Vec>) -> LayoutSplit { - let (mut spaces, split_point) = find_group_split_point(groups); - split_spaces_at(viewport_size, &mut spaces, split_point) -} - -fn find_group_split_point(groups: Vec>) -> (Vec, usize) { - assert!(groups.len() > 1); - - let num_spaces: usize = groups.iter().map(|g| g.len()).sum(); - - let mut best_split = 0; - let mut rearranged_spaces = vec![]; - for mut group in groups { - rearranged_spaces.append(&mut group); - - let split_candidate = rearranged_spaces.len(); - - // Prefer the split that is closest to the middle: - if (split_candidate as f32 / num_spaces as f32 - 0.5).abs() - < (best_split as f32 / num_spaces as f32 - 0.5).abs() - { - best_split = split_candidate; - } - } - assert_eq!(rearranged_spaces.len(), num_spaces); - assert!(0 < best_split && best_split < num_spaces,); - - (rearranged_spaces, best_split) -} - -fn suggest_split_direction( - viewport_size: egui::Vec2, - spaces: &[SpaceMakeInfo], - split_index: usize, -) -> SplitDirection { - use egui::vec2; - - assert!(0 < split_index && split_index < spaces.len()); - - let t = split_index as f32 / spaces.len() as f32; - - let desired_aspect_ratio = desired_aspect_ratio(spaces).unwrap_or(16.0 / 9.0); - - if viewport_size.x > desired_aspect_ratio * viewport_size.y { - let left = vec2(viewport_size.x * t, viewport_size.y); - let right = vec2(viewport_size.x * (1.0 - t), viewport_size.y); - SplitDirection::LeftRight { left, t, right } - } else { - let top = vec2(viewport_size.x, viewport_size.y * t); - let bottom = vec2(viewport_size.x, viewport_size.y * (1.0 - t)); - SplitDirection::TopBottom { top, t, bottom } - } -} - -fn split_spaces_at( - viewport_size: egui::Vec2, - spaces: &mut [SpaceMakeInfo], - split_index: usize, -) -> LayoutSplit { - assert!(0 < split_index && split_index < spaces.len()); - - match suggest_split_direction(viewport_size, spaces, split_index) { - SplitDirection::LeftRight { left, t, right } => { - let left = layout_by_path_prefix(left, &mut spaces[..split_index]); - let right = layout_by_path_prefix(right, &mut spaces[split_index..]); - LayoutSplit::LeftRight(left.into(), t, right.into()) - } - SplitDirection::TopBottom { top, t, bottom } => { - let top = layout_by_path_prefix(top, &mut spaces[..split_index]); - let bottom = layout_by_path_prefix(bottom, &mut spaces[split_index..]); - LayoutSplit::TopBottom(top.into(), t, bottom.into()) - } - } -} - -/// If we need to pick only one aspect ratio for all these spaces, what is a good aspect ratio? -/// -/// This is a very, VERY, rough heuristic. It really only work in a few cases: -/// -/// * All spaces have similar aspect ration (e.g. all portrait or all landscape) -/// * Only one space care about aspect ratio, and the other are flexible -/// * A mix of the above -/// -/// Still, it is better than nothing. -fn desired_aspect_ratio(spaces: &[SpaceMakeInfo]) -> Option { - // Taking the arithmetic mean of all given aspect ratios. - // It makes very little sense, unless the aspect ratios are all close already. - // Perhaps a mode or median would make more sense? - - let mut sum = 0.0; - let mut num = 0.0; - for space in spaces { - if let Some(aspect_ratio) = space.aspect_ratio { - if aspect_ratio.is_finite() { - sum += aspect_ratio; - num += 1.0; - } - } - } - - (num != 0.0).then_some(sum / num) -} - -fn group_by_category(space_infos: &[SpaceMakeInfo]) -> Vec> { - let mut groups: BTreeMap> = Default::default(); - for info in space_infos { - groups.entry(info.category).or_default().push(info.clone()); - } - groups.into_values().collect() -} - -fn group_by_path_prefix(space_infos: &[SpaceMakeInfo]) -> Vec> { - if space_infos.len() < 2 { - return vec![space_infos.to_vec()]; - } - re_tracing::profile_function!(); - - let paths = space_infos - .iter() - .map(|space_info| space_info.path.as_slice().to_vec()) + // We will arrange it like so: + // + // +-------------+ + // | | + // | | + // | | + // +-------+-----+ + // | | | + // | | | + // +-------+-----+ + // + // or like so: + // + // +-----------------------+ + // | | | + // | | | + // | +------------+ + // | | | + // | | | + // | | | + // +----------+------------+ + // + // But which space gets a full side, and which doesn't? + // Answer: we prioritize them based on a class-specific layout priority: + + spaces.sort_by_key(|smi| -(smi.layout_priority as isize)); + + let pane_ids = spaces + .into_iter() + .map(|smi| tiles.insert_pane(smi.id)) .collect_vec(); - for i in 0.. { - let mut groups: BTreeMap, Vec<&SpaceMakeInfo>> = Default::default(); - for (path, space) in paths.iter().zip(space_infos) { - groups.entry(path.get(i)).or_default().push(space); - } - if groups.len() == 1 && groups.contains_key(&None) { - break; - } - if groups.len() > 1 { - return groups - .values() - .map(|spaces| spaces.iter().cloned().cloned().collect()) - .collect(); - } - } - space_infos - .iter() - .map(|space| vec![space.clone()]) - .collect() + let inner_grid = tiles.insert_grid_tile(vec![pane_ids[1], pane_ids[2]]); + tiles.insert_grid_tile(vec![pane_ids[0], inner_grid]) } diff --git a/crates/re_viewport/src/lib.rs b/crates/re_viewport/src/lib.rs index 90ec2b3ea70e..862a0af299cb 100644 --- a/crates/re_viewport/src/lib.rs +++ b/crates/re_viewport/src/lib.rs @@ -45,7 +45,7 @@ pub mod item_ui { .re_ui .selectable_label_with_icon( ui, - space_view.class(ctx).icon(), + space_view.class(ctx.space_view_class_registry).icon(), space_view.display_name.clone(), is_selected, ) diff --git a/crates/re_viewport/src/space_view.rs b/crates/re_viewport/src/space_view.rs index 0e2ba3221d72..29fd60e8ad9e 100644 --- a/crates/re_viewport/src/space_view.rs +++ b/crates/re_viewport/src/space_view.rs @@ -77,9 +77,11 @@ impl SpaceViewBlueprint { &self.class_name } - pub fn class<'a>(&self, ctx: &ViewerContext<'a>) -> &'a dyn DynSpaceViewClass { - ctx.space_view_class_registry - .get_or_log_error(&self.class_name) + pub fn class<'a>( + &self, + space_view_class_registry: &'a re_viewer_context::SpaceViewClassRegistry, + ) -> &'a dyn DynSpaceViewClass { + space_view_class_registry.get_or_log_error(&self.class_name) } pub fn on_frame_start( @@ -164,7 +166,7 @@ impl SpaceViewBlueprint { return; } - let class = self.class(ctx); + let class = self.class(ctx.space_view_class_registry); class.prepare_populate( ctx, diff --git a/crates/re_viewport/src/viewport.rs b/crates/re_viewport/src/viewport.rs index 4b017aa5a604..d6ac0c9d5fed 100644 --- a/crates/re_viewport/src/viewport.rs +++ b/crates/re_viewport/src/viewport.rs @@ -535,12 +535,10 @@ impl Viewport { maximized_tree = egui_tiles::Tree::new(root, tiles); &mut maximized_tree } else { - if self.tree.root().is_none() { + if self.tree.is_empty() { self.tree = super::auto_layout::tree_from_space_views( - ctx, - ui.available_size(), + ctx.space_view_class_registry, &self.space_views, - &state.space_view_states, ); } &mut self.tree @@ -581,7 +579,7 @@ impl Viewport { .re_ui .selectable_label_with_icon( ui, - space_view.class(ctx).icon(), + space_view.class(ctx.space_view_class_registry).icon(), if space_view.space_origin.is_root() { space_view.display_name.clone() } else { @@ -876,7 +874,7 @@ impl<'a, 'b> egui_tiles::Behavior for TabViewer<'a, 'b> { } let help_text = space_view - .class(self.ctx) + .class(self.ctx.space_view_class_registry) .help_text(self.ctx.re_ui, space_view_state); re_ui::help_hover_button(ui).on_hover_text(help_text); } diff --git a/examples/rust/custom_space_view/src/color_coordinates_space_view.rs b/examples/rust/custom_space_view/src/color_coordinates_space_view.rs index d1d28a3d1ed5..f51441737bb4 100644 --- a/examples/rust/custom_space_view/src/color_coordinates_space_view.rs +++ b/examples/rust/custom_space_view/src/color_coordinates_space_view.rs @@ -5,8 +5,8 @@ use re_viewer::external::{ re_log_types::EntityPath, re_ui, re_viewer_context::{ - HoverHighlight, Item, SelectionHighlight, SpaceViewClass, SpaceViewClassName, SpaceViewId, - SpaceViewState, TypedScene, UiVerbosity, ViewerContext, + HoverHighlight, Item, SelectionHighlight, SpaceViewClass, SpaceViewClassLayoutPriority, + SpaceViewClassName, SpaceViewId, SpaceViewState, TypedScene, UiVerbosity, ViewerContext, }, }; @@ -95,6 +95,10 @@ impl SpaceViewClass for ColorCoordinatesSpaceView { Some(1.0) } + fn layout_priority(&self) -> SpaceViewClassLayoutPriority { + Default::default() + } + /// Additional UI displayed when the space view is selected. /// /// In this sample we show a combo box to select the color coordinates mode. diff --git a/scripts/run_all.py b/scripts/run_all.py index fad09785be7f..fd9ae8681d22 100755 --- a/scripts/run_all.py +++ b/scripts/run_all.py @@ -19,31 +19,34 @@ } HAS_NO_SAVE_ARG = { - "examples/python/multiprocessing", - "examples/python/minimal", - "examples/python/dna", "examples/python/blueprint", + "examples/python/dna", + "examples/python/minimal", + "examples/python/multiprocessing", } -def start_process(args: list[str], cwd: str, wait: bool) -> Any: +def start_process(args: list[str], *, wait: bool) -> Any: + readable_cmd = " ".join(f'"{a}"' if " " in a else a for a in args) + print(f"> {readable_cmd}") + process = subprocess.Popen( args, - cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) if wait: returncode = process.wait() if returncode != 0: - print(process.communicate()[0].decode("utf-8").rstrip()) + print(output_from_process(process)) + print() print(f"process exited with error code {returncode}") exit(returncode) return process def run_py_example(path: str, viewer_port: int | None = None, wait: bool = True, save: str | None = None) -> Any: - args = ["python3", "main.py"] + args = [os.path.join(path, "main.py")] if path in EXTRA_ARGS: args += EXTRA_ARGS[path] @@ -53,22 +56,15 @@ def run_py_example(path: str, viewer_port: int | None = None, wait: bool = True, if viewer_port is not None: args += ["--connect", f"--addr=127.0.0.1:{viewer_port}"] - cmd = " ".join(f'"{a}"' if " " in a else a for a in args) - print(f"Running example '{path}' via '{cmd}'") - return start_process( args, - cwd=path, wait=wait, ) -def run_saved_example(path: str, wait: bool = True) -> Any: - return start_process( - ["cargo", "run", "-p", "rerun-cli", "--all-features", "--", "out.rrd"], - cwd=path, - wait=wait, - ) +# stdout and stderr +def output_from_process(process: Any) -> str: + return process.communicate()[0].decode("utf-8").rstrip() def get_free_port() -> int: @@ -93,9 +89,12 @@ def collect_examples(fast: bool) -> list[str]: "examples/python/text_logging", ] else: - # ros requires complex system dependencies to be installed - # depth_sensor requires a specific piece of hardware to be attached - skip_list = ["examples/python/ros_node/main.py", "examples/python/live_depth_sensor/main.py"] + skip_list = [ + # depth_sensor requires a specific piece of hardware to be attached + "examples/python/live_depth_sensor/main.py", + # ros requires complex system dependencies to be installed + "examples/python/ros_node/main.py", + ] return [ os.path.dirname(main_path) for main_path in glob("examples/python/**/main.py") if main_path not in skip_list @@ -103,7 +102,8 @@ def collect_examples(fast: bool) -> list[str]: def print_example_output(path: str, example: Any) -> None: - print(f"\nExample {path}:\n{example.communicate()[0].decode('utf-8').rstrip()}") + output = example.communicate()[0].decode("utf-8").rstrip() + print(f"\nExample {path}:\n{output}\n") class Viewer: @@ -169,15 +169,16 @@ def run_sdk_build() -> None: assert returncode == 0, f"process exited with error code {returncode}" -def run_viewer_build() -> None: - print("Building rerun…") +def run_viewer_build(web: bool) -> None: + print("Building Rerun Viewer…") returncode = subprocess.Popen( [ "cargo", "build", "-p", "rerun-cli", - "--all-features", + "--no-default-features", + "--features=web_viewer" if web else "--features=native_viewer", "--quiet", ] ).wait() @@ -185,87 +186,98 @@ def run_viewer_build() -> None: def run_web(examples: list[str], separate: bool) -> None: - if not separate: + if separate: + entries: list[tuple[str, Any, Any]] = [] + # start all examples in parallel + for path in examples: + # each example gets its own viewer + viewer = Viewer(web=True).start() + example = run_py_example(path, viewer_port=viewer.sdk_port, wait=False) + entries.append((path, viewer, example)) + + # wait for examples to finish logging + for entry in entries: + _, _, example = entry + example.wait() + + # give servers/viewers a moment to finish loading data + time.sleep(5) + + # shut down servers/viewers + for entry in entries: + path, viewer, example = entry + print_example_output(path, example) + viewer.close() + + else: with Viewer(close=True, web=True) as viewer: for path in examples: - example = run_py_example(path, viewer_port=viewer.sdk_port) - print_example_output(path, example) - return - - entries: list[tuple[str, Any, Any]] = [] - # start all examples in parallel - for path in examples: - # each example gets its own viewer - viewer = Viewer(web=True).start() - example = run_py_example(path, viewer_port=viewer.sdk_port, wait=False) - entries.append((path, viewer, example)) - - # wait for examples to finish logging - for entry in entries: - _, _, example = entry - example.wait() - - # give servers/viewers a moment to finish loading data - time.sleep(5) - - # shut down servers/viewers - for entry in entries: - path, viewer, example = entry - print_example_output(path, example) - viewer.close() + process = run_py_example(path, viewer_port=viewer.sdk_port) + print(f"{output_from_process(process)}\n") def run_save(examples: list[str]) -> None: for path in examples: if path not in HAS_NO_SAVE_ARG: - example = run_py_example(path, save="out.rrd") - print_example_output(path, example) + process = run_py_example(path, save="out.rrd") + print(f"{output_from_process(process)}\n") + + +def run_saved_example(path: str, wait: bool) -> Any: + return start_process( + [ + "cargo", + "rerun", + os.path.join(path, "out.rrd"), + ], + wait=wait, + ) def run_load(examples: list[str], separate: bool, close: bool) -> None: - if not separate: + examples = [path for path in examples if path not in HAS_NO_SAVE_ARG] + + if separate: + entries: list[tuple[str, Any]] = [] + for path in examples: + example = run_saved_example(path, wait=False) + entries.append((path, example)) + + for entry in entries: + path, example = entry + print_example_output(path, example) + if close: + example.kill() + else: # run all examples sequentially for path in examples: # each one must be closed for the next one to start running - example = run_saved_example(path) - print_example_output(path, example) - return - - entries: list[tuple[str, Any]] = [] - for path in examples: - example = run_saved_example(path, wait=False) - entries.append((path, example)) - - for entry in entries: - path, example = entry - print_example_output(path, example) - if close: - example.kill() + process = run_saved_example(path, wait=True) + print(f"{output_from_process(process)}\n") def run_native(examples: list[str], separate: bool, close: bool) -> None: - if not separate: + if separate: + # start all examples in parallel: + cleanup: list[tuple[Any, Any]] = [] + for path in examples: + # each example gets its own viewer + viewer = Viewer().start() + example = run_py_example(path, viewer.sdk_port, False) + cleanup.append((viewer, example)) + + # wait for all processes to finish, and close the viewers if requested + for pair in cleanup: + viewer, example = pair + print_example_output(path, example) + if close: + viewer.close() + else: # run all examples sequentially in a single viewer with Viewer(close) as viewer: for path in examples: - example = run_py_example(path, viewer_port=viewer.sdk_port, wait=True) - print_example_output(path, example) - return - - cleanup: list[tuple[Any, Any]] = [] - # start all examples in parallel - for path in examples: - # each example gets its own viewer - viewer = Viewer().start() - example = run_py_example(path, viewer.sdk_port, False) - cleanup.append((viewer, example)) - - # wait for all processes to finish, and close the viewers if requested - for pair in cleanup: - viewer, example = pair - print_example_output(path, example) - if close: - viewer.close() + process = run_py_example(path, viewer_port=viewer.sdk_port, wait=True) + print(f"{output_from_process(process)}\n") def main() -> None: @@ -289,8 +301,10 @@ def main() -> None: examples = collect_examples(args.fast) if not args.skip_build: - run_sdk_build() - run_viewer_build() + if not args.load: + run_sdk_build() + if not args.save: + run_viewer_build(args.web) if args.web: run_web(examples, separate=args.separate)