From f9d8a991058e16d9a59395c87aae76d757eb5958 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Thu, 8 Jun 2023 14:36:56 +0200 Subject: [PATCH] Separate crate for time series space view (#2324) ### What Changed space view registry a little bit to make it easy to report failure to find a type (`get_and_log_error`) ### Checklist * [x] I have read and agree to [Contributor Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and the [Code of Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md) PR Build Summary: https://build.rerun.io/pr/2324 Docs preview: https://rerun.io/preview/9c37b91/docs Examples preview: https://rerun.io/preview/9c37b91/examples --- Cargo.lock | 22 +- Cargo.toml | 1 + crates/re_space_view_time_series/Cargo.toml | 32 +++ crates/re_space_view_time_series/README.md | 11 + crates/re_space_view_time_series/src/lib.rs | 8 + .../src/scene_part.rs} | 47 ++- .../src/space_view_class.rs | 270 ++++++++++++++++++ crates/re_viewer/Cargo.toml | 1 + crates/re_viewer/src/app.rs | 7 +- .../space_view/space_view_class_registry.rs | 25 +- crates/re_viewport/Cargo.toml | 2 - crates/re_viewport/src/auto_layout.rs | 3 +- crates/re_viewport/src/lib.rs | 1 - crates/re_viewport/src/space_view.rs | 58 ++-- .../re_viewport/src/view_time_series/mod.rs | 5 - crates/re_viewport/src/view_time_series/ui.rs | 242 ---------------- crates/re_viewport/src/viewport.rs | 30 +- scripts/publish_crates.sh | 3 +- 18 files changed, 426 insertions(+), 342 deletions(-) create mode 100644 crates/re_space_view_time_series/Cargo.toml create mode 100644 crates/re_space_view_time_series/README.md create mode 100644 crates/re_space_view_time_series/src/lib.rs rename crates/{re_viewport/src/view_time_series/scene.rs => re_space_view_time_series/src/scene_part.rs} (85%) create mode 100644 crates/re_space_view_time_series/src/space_view_class.rs delete mode 100644 crates/re_viewport/src/view_time_series/mod.rs delete mode 100644 crates/re_viewport/src/view_time_series/ui.rs diff --git a/Cargo.lock b/Cargo.lock index 9ed141fcb456..ff02afd09a31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4346,6 +4346,25 @@ dependencies = [ "vec1", ] +[[package]] +name = "re_space_view_time_series" +version = "0.7.0-alpha.0" +dependencies = [ + "egui", + "re_arrow_store", + "re_components", + "re_format", + "re_log", + "re_log_types", + "re_query", + "re_renderer", + "re_space_view", + "re_tracing", + "re_ui", + "re_viewer_context", + "vec1", +] + [[package]] name = "re_string_interner" version = "0.7.0-alpha.0" @@ -4463,6 +4482,7 @@ dependencies = [ "re_space_view_spatial", "re_space_view_text", "re_space_view_text_box", + "re_space_view_time_series", "re_time_panel", "re_tracing", "re_ui", @@ -4538,10 +4558,8 @@ dependencies = [ "re_components", "re_data_store", "re_data_ui", - "re_format", "re_log", "re_log_types", - "re_query", "re_renderer", "re_space_view", "re_tensor_ops", diff --git a/Cargo.toml b/Cargo.toml index e8a808583e87..b806abc6305c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ re_space_view_bar_chart = { path = "crates/re_space_view_bar_chart", version = " re_space_view_spatial = { path = "crates/re_space_view_spatial", version = "0.7.0-alpha.0", default-features = false } re_space_view_text = { path = "crates/re_space_view_text", version = "0.7.0-alpha.0", default-features = false } re_space_view_text_box = { path = "crates/re_space_view_text_box", version = "0.7.0-alpha.0", default-features = false } +re_space_view_time_series = { path = "crates/re_space_view_time_series", version = "0.7.0-alpha.0", default-features = false } re_string_interner = { path = "crates/re_string_interner", version = "0.7.0-alpha.0", default-features = false } re_tensor_ops = { path = "crates/re_tensor_ops", version = "0.7.0-alpha.0", default-features = false } re_time_panel = { path = "crates/re_time_panel", version = "=0.7.0-alpha.0", default-features = false } diff --git a/crates/re_space_view_time_series/Cargo.toml b/crates/re_space_view_time_series/Cargo.toml new file mode 100644 index 000000000000..08aae9d3ff70 --- /dev/null +++ b/crates/re_space_view_time_series/Cargo.toml @@ -0,0 +1,32 @@ +[package] +authors.workspace = true +description = "A Space View that shows plots over Rerun timelines." +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "re_space_view_time_series" +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +re_arrow_store.workspace = true +re_components.workspace = true +re_log_types.workspace = true +re_log.workspace = true +re_renderer.workspace = true +re_space_view.workspace = true +re_tracing.workspace = true +re_ui.workspace = true +re_viewer_context.workspace = true +re_query.workspace = true +re_format.workspace = true + +egui.workspace = true +vec1.workspace = true diff --git a/crates/re_space_view_time_series/README.md b/crates/re_space_view_time_series/README.md new file mode 100644 index 000000000000..140f554d0a2f --- /dev/null +++ b/crates/re_space_view_time_series/README.md @@ -0,0 +1,11 @@ +# re_space_view_bar_chart + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_space_view_bar_chart.svg)](https://crates.io/crates/re_space_view_bar_chart) +[![Documentation](https://docs.rs/re_space_view_bar_chart/badge.svg)](https://docs.rs/re_space_view_bar_chart) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +A Space View that shows plots over Rerun timelines. + diff --git a/crates/re_space_view_time_series/src/lib.rs b/crates/re_space_view_time_series/src/lib.rs new file mode 100644 index 000000000000..17b566df8711 --- /dev/null +++ b/crates/re_space_view_time_series/src/lib.rs @@ -0,0 +1,8 @@ +//! Rerun time series Space View +//! +//! A Space View that shows plots over Rerun timelines. + +mod scene_part; +mod space_view_class; + +pub use space_view_class::TimeSeriesSpaceView; diff --git a/crates/re_viewport/src/view_time_series/scene.rs b/crates/re_space_view_time_series/src/scene_part.rs similarity index 85% rename from crates/re_viewport/src/view_time_series/scene.rs rename to crates/re_space_view_time_series/src/scene_part.rs index 6fe5e4e48f3f..a1c4e4195df1 100644 --- a/crates/re_viewport/src/view_time_series/scene.rs +++ b/crates/re_space_view_time_series/src/scene_part.rs @@ -1,9 +1,9 @@ use re_arrow_store::TimeRange; -use re_log_types::{Component, InstanceKey}; +use re_log_types::{Component, ComponentName, InstanceKey}; use re_query::{range_entity_with_primary, QueryError}; -use re_viewer_context::{AnnotationMap, DefaultColor, SceneQuery, ViewerContext}; +use re_viewer_context::{AnnotationMap, DefaultColor, ScenePart, SceneQuery, ViewerContext}; -// --- +use crate::TimeSeriesSpaceView; #[derive(Clone, Debug)] pub struct PlotPointAttrs { @@ -59,17 +59,41 @@ pub struct SceneTimeSeries { pub lines: Vec, } -impl SceneTimeSeries { - /// Loads all plots into the scene according to the given query. - pub(crate) fn load(&mut self, ctx: &mut ViewerContext<'_>, query: &SceneQuery<'_>) { +impl ScenePart for SceneTimeSeries { + fn archetype(&self) -> re_viewer_context::ArchetypeDefinition { + vec1::Vec1::try_from(Self::archetype_array()).unwrap() // TODO(wumpf): `archetype` should return a fixed sized array. + } + + fn populate( + &mut self, + ctx: &mut ViewerContext<'_>, + query: &SceneQuery<'_>, + _space_view_state: &::State, + _scene_context: &::Context, + _highlights: &re_viewer_context::SpaceViewHighlights, + ) -> Vec { re_tracing::profile_function!(); self.annotation_map.load(ctx, query); self.load_scalars(ctx, query); + + Vec::new() + } +} + +impl SceneTimeSeries { + fn archetype_array() -> [ComponentName; 6] { + [ + InstanceKey::name(), + re_components::Scalar::name(), + re_components::ScalarPlotProps::name(), + re_components::ColorRGBA::name(), + re_components::Radius::name(), + re_components::Label::name(), + ] } - #[inline(never)] // Better callstacks on crashes fn load_scalars(&mut self, ctx: &mut ViewerContext<'_>, query: &SceneQuery<'_>) { re_tracing::profile_function!(); @@ -88,14 +112,7 @@ impl SceneTimeSeries { TimeRange::new(i64::MIN.into(), i64::MAX.into()), ); - let components = [ - InstanceKey::name(), - re_components::Scalar::name(), - re_components::ScalarPlotProps::name(), - re_components::ColorRGBA::name(), - re_components::Radius::name(), - re_components::Label::name(), - ]; + let components = Self::archetype_array(); let ent_views = range_entity_with_primary::( store, &query, ent_path, components, ); 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 new file mode 100644 index 000000000000..2c04654c56e6 --- /dev/null +++ b/crates/re_space_view_time_series/src/space_view_class.rs @@ -0,0 +1,270 @@ +use egui::{ + plot::{Legend, Line, Plot, Points}, + Color32, +}; + +use re_arrow_store::TimeType; +use re_format::next_grid_tick_magnitude_ns; +use re_log_types::EntityPath; +use re_space_view::{controls, EmptySpaceViewState}; +use re_viewer_context::{ + SpaceViewClass, SpaceViewClassName, SpaceViewId, TypedScene, ViewerContext, +}; + +use crate::scene_part::{PlotSeriesKind, SceneTimeSeries}; + +#[derive(Default)] +pub struct TimeSeriesSpaceView; + +impl SpaceViewClass for TimeSeriesSpaceView { + type State = EmptySpaceViewState; + type Context = re_space_view::EmptySceneContext; + type SceneParts = SceneTimeSeries; + type ScenePartData = (); + + fn name(&self) -> SpaceViewClassName { + "Time Series".into() + } + + fn icon(&self) -> &'static re_ui::Icon { + &re_ui::icons::SPACE_VIEW_HISTOGRAM + } + + fn help_text(&self, re_ui: &re_ui::ReUi, _state: &Self::State) -> egui::WidgetText { + let mut layout = re_ui::LayoutJobBuilder::new(re_ui); + + layout.add("Pan by dragging, or scroll (+ "); + layout.add(controls::HORIZONTAL_SCROLL_MODIFIER); + layout.add(" for horizontal).\n"); + + layout.add("Zoom with pinch gesture or scroll + "); + layout.add(controls::ZOOM_SCROLL_MODIFIER); + layout.add(".\n"); + + layout.add("Drag "); + layout.add(controls::SELECTION_RECT_ZOOM_BUTTON); + layout.add(" to zoom in/out using a selection.\n"); + + layout.add("Click "); + layout.add(controls::MOVE_TIME_CURSOR_BUTTON); + layout.add(" to move the time cursor.\n\n"); + + layout.add_button_text(controls::RESET_VIEW_BUTTON_TEXT); + layout.add(" to reset the view."); + + layout.layout_job.into() + } + + fn preferred_tile_aspect_ratio(&self, _state: &Self::State) -> Option { + None + } + + fn selection_ui( + &self, + _ctx: &mut ViewerContext<'_>, + _ui: &mut egui::Ui, + _state: &mut Self::State, + _space_origin: &EntityPath, + _space_view_id: SpaceViewId, + ) { + } + + fn ui( + &self, + ctx: &mut ViewerContext<'_>, + ui: &mut egui::Ui, + _state: &mut Self::State, + scene: &mut TypedScene, + _space_origin: &EntityPath, + _space_view_id: SpaceViewId, + ) { + re_tracing::profile_function!(); + + let time_ctrl = &ctx.rec_cfg.time_ctrl; + let current_time = time_ctrl.time_i64(); + let time_type = time_ctrl.time_type(); + let timeline = time_ctrl.timeline(); + + let timeline_name = timeline.name().to_string(); + + // Compute the minimum time/X value for the entire plot… + let min_time = scene + .parts + .lines + .iter() + .flat_map(|line| line.points.iter().map(|p| p.0)) + .min() + .unwrap_or(0); + + // …then use that as an offset to avoid nasty precision issues with + // large times (nanos since epoch does not fit into an f64). + let time_offset = if timeline.typ() == TimeType::Time { + // In order to make the tick-marks on the time axis fall on whole days, hours, minutes etc, + // we need to round to a whole day: + round_ns_to_start_of_day(min_time) + } else { + min_time + }; + + // use timeline_name as part of id, so that egui stores different pan/zoom for different timelines + let plot_id_src = ("plot", &timeline_name); + + let mut plot = Plot::new(plot_id_src) + .legend(Legend { + position: egui::plot::Corner::RightBottom, + ..Default::default() + }) + .x_axis_formatter(move |time, _| format_time(time_type, time as i64 + time_offset)) + .label_formatter(move |name, value| { + let name = if name.is_empty() { "y" } else { name }; + let is_integer = value.y.round() == value.y; + let decimals = if is_integer { 0 } else { 5 }; + format!( + "{timeline_name}: {}\n{name}: {:.*}", + time_type.format((value.x as i64 + time_offset).into()), + decimals, + value.y, + ) + }); + + if timeline.typ() == TimeType::Time { + let canvas_size = ui.available_size(); + plot = plot.x_grid_spacer(move |spacer| ns_grid_spacer(canvas_size, &spacer)); + } + + let egui::plot::PlotResponse { + inner: time_x, + response, + transform, + } = plot.show(ui, |plot_ui| { + if plot_ui.plot_secondary_clicked() { + let timeline = ctx.rec_cfg.time_ctrl.timeline(); + ctx.rec_cfg.time_ctrl.set_timeline_and_time( + *timeline, + plot_ui.pointer_coordinate().unwrap().x as i64 + time_offset, + ); + ctx.rec_cfg.time_ctrl.pause(); + } + + for line in &scene.parts.lines { + let points = line + .points + .iter() + .map(|p| [(p.0 - time_offset) as _, p.1]) + .collect::>(); + + let c = line.color; + let color = Color32::from_rgba_premultiplied(c[0], c[1], c[2], c[3]); + + match line.kind { + PlotSeriesKind::Continuous => plot_ui.line( + Line::new(points) + .name(&line.label) + .color(color) + .width(line.width), + ), + PlotSeriesKind::Scatter => plot_ui.points( + Points::new(points) + .name(&line.label) + .color(color) + .radius(line.width), + ), + } + } + + current_time.map(|current_time| { + let time_x = (current_time - time_offset) as f64; + plot_ui.screen_from_plot([time_x, 0.0].into()).x + }) + }); + + if let Some(time_x) = time_x { + let interact_radius = ui.style().interaction.resize_grab_radius_side; + let line_rect = egui::Rect::from_x_y_ranges(time_x..=time_x, response.rect.y_range()) + .expand(interact_radius); + + let time_drag_id = ui.id().with("time_drag"); + let response = ui + .interact(line_rect, time_drag_id, egui::Sense::drag()) + .on_hover_and_drag_cursor(egui::CursorIcon::ResizeHorizontal); + + if response.dragged() { + if let Some(pointer_pos) = ui.input(|i| i.pointer.hover_pos()) { + let time = + time_offset + transform.value_from_position(pointer_pos).x.round() as i64; + + let time_ctrl = &mut ctx.rec_cfg.time_ctrl; + time_ctrl.set_time(time); + time_ctrl.pause(); + } + } + + let stroke = if response.dragged() { + ui.style().visuals.widgets.active.fg_stroke + } else if response.hovered() { + ui.style().visuals.widgets.hovered.fg_stroke + } else { + ui.visuals().widgets.inactive.fg_stroke + }; + ctx.re_ui + .paint_time_cursor(ui.painter(), time_x, response.rect.y_range(), stroke); + } + } +} + +fn format_time(time_type: TimeType, time_int: i64) -> String { + if time_type == TimeType::Time { + let time = re_log_types::Time::from_ns_since_epoch(time_int); + time.format_time_compact() + } else { + time_type.format(re_log_types::TimeInt::from(time_int)) + } +} + +fn ns_grid_spacer( + canvas_size: egui::Vec2, + input: &egui::plot::GridInput, +) -> Vec { + let minimum_medium_line_spacing = 150.0; // ≈min size of a label + let max_medium_lines = canvas_size.x as f64 / minimum_medium_line_spacing; + + let (min_ns, max_ns) = input.bounds; + let width_ns = max_ns - min_ns; + + let mut small_spacing_ns = 1; + while width_ns / (next_grid_tick_magnitude_ns(small_spacing_ns) as f64) > max_medium_lines { + small_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns); + } + let medium_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns); + let big_spacing_ns = next_grid_tick_magnitude_ns(medium_spacing_ns); + + let mut current_ns = (min_ns.floor() as i64) / small_spacing_ns * small_spacing_ns; + let mut marks = vec![]; + + while current_ns <= max_ns.ceil() as i64 { + let is_big_line = current_ns % big_spacing_ns == 0; + let is_medium_line = current_ns % medium_spacing_ns == 0; + + let step_size = if is_big_line { + big_spacing_ns + } else if is_medium_line { + medium_spacing_ns + } else { + small_spacing_ns + }; + + marks.push(egui::plot::GridMark { + value: current_ns as f64, + step_size: step_size as f64, + }); + + current_ns += small_spacing_ns; + } + + marks +} + +fn round_ns_to_start_of_day(ns: i64) -> i64 { + let ns_per_day = 24 * 60 * 60 * 1_000_000_000; + (ns + ns_per_day / 2) / ns_per_day * ns_per_day +} diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml index 144a7f5f0a17..2b8743366d27 100644 --- a/crates/re_viewer/Cargo.toml +++ b/crates/re_viewer/Cargo.toml @@ -55,6 +55,7 @@ re_space_view_bar_chart.workspace = true re_space_view_spatial.workspace = true re_space_view_text_box.workspace = true re_space_view_text.workspace = true +re_space_view_time_series.workspace = true re_time_panel.workspace = true re_tracing.workspace = true re_ui = { workspace = true, features = ["eframe"] } diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 612527429de0..fb2de5fbfba2 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -940,10 +940,11 @@ impl eframe::App for App { fn populate_space_view_class_registry_with_builtin( space_view_class_registry: &mut SpaceViewClassRegistry, ) -> Result<(), SpaceViewClassRegistryError> { - space_view_class_registry.add::()?; - space_view_class_registry.add::()?; - space_view_class_registry.add::()?; space_view_class_registry.add::()?; + space_view_class_registry.add::()?; + space_view_class_registry.add::()?; + space_view_class_registry.add::()?; + space_view_class_registry.add::()?; Ok(()) } diff --git a/crates/re_viewer_context/src/space_view/space_view_class_registry.rs b/crates/re_viewer_context/src/space_view/space_view_class_registry.rs index 9c8b648e7d41..fa4e6b402659 100644 --- a/crates/re_viewer_context/src/space_view/space_view_class_registry.rs +++ b/crates/re_viewer_context/src/space_view/space_view_class_registry.rs @@ -6,9 +6,6 @@ use crate::{DynSpaceViewClass, SpaceViewClassName}; pub enum SpaceViewClassRegistryError { #[error("Space view with typename \"{0}\" was already registered.")] DuplicateTypeName(SpaceViewClassName), - - #[error("Space view with typename \"{0}\" was not found.")] - TypeNotFound(SpaceViewClassName), } /// Registry of all known space view types. @@ -37,15 +34,19 @@ impl SpaceViewClassRegistry { Ok(()) } - /// Queries a space view type by name. - pub fn get( - &self, - name: SpaceViewClassName, - ) -> Result<&dyn DynSpaceViewClass, SpaceViewClassRegistryError> { - self.0 - .get(&name) - .map(|boxed| boxed.as_ref()) - .ok_or(SpaceViewClassRegistryError::TypeNotFound(name)) + /// Queries a Space View type by class name. + fn get(&self, name: SpaceViewClassName) -> Option<&dyn DynSpaceViewClass> { + self.0.get(&name).map(|boxed| boxed.as_ref()) + } + + /// Queries a Space View type by class name and logs if it fails. + pub fn get_or_log_error(&self, name: SpaceViewClassName) -> Option<&dyn DynSpaceViewClass> { + let result = self.get(name); + // TODO(wumpf): Workaround for tensor not yet ported + if result.is_none() && name != "Tensor" { + re_log::error_once!("Unknown space view class {:?}", name); + } + result } pub fn iter(&self) -> impl Iterator { diff --git a/crates/re_viewport/Cargo.toml b/crates/re_viewport/Cargo.toml index 508813ebd293..690fac2b3648 100644 --- a/crates/re_viewport/Cargo.toml +++ b/crates/re_viewport/Cargo.toml @@ -20,10 +20,8 @@ re_arrow_store.workspace = true re_components = { workspace = true, features = ["ecolor", "glam", "image"] } re_data_store = { workspace = true, features = ["serde"] } re_data_ui.workspace = true -re_format.workspace = true re_log_types.workspace = true re_log.workspace = true -re_query.workspace = true re_renderer = { workspace = true, default-features = false, features = [ "arrow", "import-gltf", diff --git a/crates/re_viewport/src/auto_layout.rs b/crates/re_viewport/src/auto_layout.rs index 5d0b51bf7a05..123b62eceb84 100644 --- a/crates/re_viewport/src/auto_layout.rs +++ b/crates/re_viewport/src/auto_layout.rs @@ -68,8 +68,7 @@ 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| { ctx.space_view_class_registry - .get(space_view.class) - .ok() + .get_or_log_error(space_view.class) .and_then(|class| class.preferred_tile_aspect_ratio(state.state.as_ref())) }); diff --git a/crates/re_viewport/src/lib.rs b/crates/re_viewport/src/lib.rs index f7b73a30ff4e..a054495a6a69 100644 --- a/crates/re_viewport/src/lib.rs +++ b/crates/re_viewport/src/lib.rs @@ -10,7 +10,6 @@ mod space_view_heuristics; mod space_view_highlights; mod view_category; mod view_tensor; -mod view_time_series; mod viewport; pub mod blueprint_components; diff --git a/crates/re_viewport/src/space_view.rs b/crates/re_viewport/src/space_view.rs index 7ac6b4ba72c4..46746f864525 100644 --- a/crates/re_viewport/src/space_view.rs +++ b/crates/re_viewport/src/space_view.rs @@ -8,7 +8,7 @@ use crate::{ space_info::SpaceInfoCollection, space_view_heuristics::default_queried_entities, view_category::{categorize_entity_path, ViewCategory}, - view_tensor, view_time_series, + view_tensor, }; // ---------------------------------------------------------------------------- @@ -144,8 +144,8 @@ impl SpaceViewBlueprint { ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui, ) { - if let Ok(space_view_class) = ctx.space_view_class_registry.get(self.class) { - re_tracing::profile_scope!("selection_ui", space_view_class.name()); + re_tracing::profile_function!(); + if let Some(space_view_class) = ctx.space_view_class_registry.get_or_log_error(self.class) { space_view_class.selection_ui( ctx, ui, @@ -155,14 +155,15 @@ impl SpaceViewBlueprint { ); } else { // Legacy handling - #[allow(clippy::match_same_arms)] match self.category { - ViewCategory::Text | ViewCategory::Spatial | ViewCategory::TextBox => { + ViewCategory::Text + | ViewCategory::Spatial + | ViewCategory::TextBox + | ViewCategory::TimeSeries + | ViewCategory::BarChart => { // migrated. } - ViewCategory::TimeSeries => {} - ViewCategory::BarChart => {} ViewCategory::Tensor => { if let Some(selected_tensor) = &view_state.selected_tensor { if let Some(state_tensor) = @@ -191,7 +192,7 @@ impl SpaceViewBlueprint { return; } - if let Ok(space_view_class) = ctx.space_view_class_registry.get(self.class) { + if let Some(space_view_class) = ctx.space_view_class_registry.get_or_log_error(self.class) { space_view_class.prepare_populate( ctx, view_state.state.as_mut(), @@ -210,14 +211,16 @@ impl SpaceViewBlueprint { let mut scene = space_view_class.new_scene(); scene.populate(ctx, &query, view_state.state.as_ref(), highlights); - space_view_class.ui( - ctx, - ui, - view_state.state.as_mut(), - scene, - &self.space_origin, - self.id, - ); + ui.scope(|ui| { + space_view_class.ui( + ctx, + ui, + view_state.state.as_mut(), + scene, + &self.space_origin, + self.id, + ); + }); } else { // Legacy handling let query = re_viewer_context::SceneQuery { @@ -232,16 +235,11 @@ impl SpaceViewBlueprint { ViewCategory::Text | ViewCategory::TextBox | ViewCategory::Spatial - | ViewCategory::BarChart => { + | ViewCategory::BarChart + | ViewCategory::TimeSeries => { // migrated. } - ViewCategory::TimeSeries => { - let mut scene = view_time_series::SceneTimeSeries::default(); - scene.load(ctx, &query); - view_state.ui_time_series(ctx, ui, &scene); - } - ViewCategory::Tensor => { let mut scene = view_tensor::SceneTensor::default(); scene.load(ctx, &query); @@ -307,7 +305,6 @@ pub struct SpaceViewState { /// Selects in [`Self::state_tensors`]. pub selected_tensor: Option, - pub state_time_series: view_time_series::ViewTimeSeriesState, pub state_tensors: ahash::HashMap, } @@ -354,17 +351,4 @@ impl SpaceViewState { } } } - - fn ui_time_series( - &mut self, - ctx: &mut ViewerContext<'_>, - ui: &mut egui::Ui, - scene: &view_time_series::SceneTimeSeries, - ) { - ui.vertical(|ui| { - ui.scope(|ui| { - view_time_series::view_time_series(ctx, ui, &mut self.state_time_series, scene); - }); - }); - } } diff --git a/crates/re_viewport/src/view_time_series/mod.rs b/crates/re_viewport/src/view_time_series/mod.rs deleted file mode 100644 index 83dd3d6ceb16..000000000000 --- a/crates/re_viewport/src/view_time_series/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod scene; -pub(crate) use self::scene::SceneTimeSeries; - -mod ui; -pub(crate) use self::ui::{help_text, view_time_series, ViewTimeSeriesState}; diff --git a/crates/re_viewport/src/view_time_series/ui.rs b/crates/re_viewport/src/view_time_series/ui.rs deleted file mode 100644 index a80559266aa3..000000000000 --- a/crates/re_viewport/src/view_time_series/ui.rs +++ /dev/null @@ -1,242 +0,0 @@ -use egui::{ - plot::{Legend, Line, Plot, Points}, - Color32, -}; - -use re_arrow_store::TimeType; -use re_format::next_grid_tick_magnitude_ns; -use re_space_view::controls::{ - HORIZONTAL_SCROLL_MODIFIER, MOVE_TIME_CURSOR_BUTTON, RESET_VIEW_BUTTON_TEXT, - SELECTION_RECT_ZOOM_BUTTON, ZOOM_SCROLL_MODIFIER, -}; -use re_viewer_context::ViewerContext; - -use super::SceneTimeSeries; -use crate::view_time_series::scene::PlotSeriesKind; - -// --- - -pub fn help_text(re_ui: &re_ui::ReUi) -> egui::WidgetText { - let mut layout = re_ui::LayoutJobBuilder::new(re_ui); - - layout.add("Pan by dragging, or scroll (+ "); - layout.add(HORIZONTAL_SCROLL_MODIFIER); - layout.add(" for horizontal).\n"); - - layout.add("Zoom with pinch gesture or scroll + "); - layout.add(ZOOM_SCROLL_MODIFIER); - layout.add(".\n"); - - layout.add("Drag "); - layout.add(SELECTION_RECT_ZOOM_BUTTON); - layout.add(" to zoom in/out using a selection.\n"); - - layout.add("Click "); - layout.add(MOVE_TIME_CURSOR_BUTTON); - layout.add(" to move the time cursor.\n\n"); - - layout.add_button_text(RESET_VIEW_BUTTON_TEXT); - layout.add(" to reset the view."); - - layout.layout_job.into() -} - -#[derive(Clone, Default, PartialEq, Eq)] -pub struct ViewTimeSeriesState; - -pub(crate) fn view_time_series( - ctx: &mut ViewerContext<'_>, - ui: &mut egui::Ui, - _state: &mut ViewTimeSeriesState, - scene: &SceneTimeSeries, -) -> egui::Response { - re_tracing::profile_function!(); - - let time_ctrl = &ctx.rec_cfg.time_ctrl; - let current_time = time_ctrl.time_i64(); - let time_type = time_ctrl.time_type(); - let timeline = time_ctrl.timeline(); - - let timeline_name = timeline.name().to_string(); - - // Compute the minimum time/X value for the entire plot… - let min_time = scene - .lines - .iter() - .flat_map(|line| line.points.iter().map(|p| p.0)) - .min() - .unwrap_or(0); - - // …then use that as an offset to avoid nasty precision issues with - // large times (nanos since epoch does not fit into an f64). - let time_offset = if timeline.typ() == TimeType::Time { - // In order to make the tick-marks on the time axis fall on whole days, hours, minutes etc, - // we need to round to a whole day: - round_ns_to_start_of_day(min_time) - } else { - min_time - }; - - // use timeline_name as part of id, so that egui stores different pan/zoom for different timelines - let plot_id_src = ("plot", &timeline_name); - - let mut plot = Plot::new(plot_id_src) - .legend(Legend { - position: egui::plot::Corner::RightBottom, - ..Default::default() - }) - .x_axis_formatter(move |time, _| format_time(time_type, time as i64 + time_offset)) - .label_formatter(move |name, value| { - let name = if name.is_empty() { "y" } else { name }; - let is_integer = value.y.round() == value.y; - let decimals = if is_integer { 0 } else { 5 }; - format!( - "{timeline_name}: {}\n{name}: {:.*}", - time_type.format((value.x as i64 + time_offset).into()), - decimals, - value.y, - ) - }); - - if timeline.typ() == TimeType::Time { - let canvas_size = ui.available_size(); - plot = plot.x_grid_spacer(move |spacer| ns_grid_spacer(canvas_size, &spacer)); - } - - let egui::plot::PlotResponse { - inner: time_x, - response, - transform, - } = plot.show(ui, |plot_ui| { - if plot_ui.plot_secondary_clicked() { - let timeline = ctx.rec_cfg.time_ctrl.timeline(); - ctx.rec_cfg.time_ctrl.set_timeline_and_time( - *timeline, - plot_ui.pointer_coordinate().unwrap().x as i64 + time_offset, - ); - ctx.rec_cfg.time_ctrl.pause(); - } - - for line in &scene.lines { - let points = line - .points - .iter() - .map(|p| [(p.0 - time_offset) as _, p.1]) - .collect::>(); - - let c = line.color; - let color = Color32::from_rgba_premultiplied(c[0], c[1], c[2], c[3]); - - match line.kind { - PlotSeriesKind::Continuous => plot_ui.line( - Line::new(points) - .name(&line.label) - .color(color) - .width(line.width), - ), - PlotSeriesKind::Scatter => plot_ui.points( - Points::new(points) - .name(&line.label) - .color(color) - .radius(line.width), - ), - } - } - - current_time.map(|current_time| { - let time_x = (current_time - time_offset) as f64; - plot_ui.screen_from_plot([time_x, 0.0].into()).x - }) - }); - - if let Some(time_x) = time_x { - let interact_radius = ui.style().interaction.resize_grab_radius_side; - let line_rect = egui::Rect::from_x_y_ranges(time_x..=time_x, response.rect.y_range()) - .expand(interact_radius); - - let time_drag_id = ui.id().with("time_drag"); - let response = ui - .interact(line_rect, time_drag_id, egui::Sense::drag()) - .on_hover_and_drag_cursor(egui::CursorIcon::ResizeHorizontal); - - if response.dragged() { - if let Some(pointer_pos) = ui.input(|i| i.pointer.hover_pos()) { - let time = - time_offset + transform.value_from_position(pointer_pos).x.round() as i64; - - let time_ctrl = &mut ctx.rec_cfg.time_ctrl; - time_ctrl.set_time(time); - time_ctrl.pause(); - } - } - - let stroke = if response.dragged() { - ui.style().visuals.widgets.active.fg_stroke - } else if response.hovered() { - ui.style().visuals.widgets.hovered.fg_stroke - } else { - ui.visuals().widgets.inactive.fg_stroke - }; - ctx.re_ui - .paint_time_cursor(ui.painter(), time_x, response.rect.y_range(), stroke); - } - - response -} - -fn format_time(time_type: TimeType, time_int: i64) -> String { - if time_type == TimeType::Time { - let time = re_log_types::Time::from_ns_since_epoch(time_int); - time.format_time_compact() - } else { - time_type.format(re_log_types::TimeInt::from(time_int)) - } -} - -fn ns_grid_spacer( - canvas_size: egui::Vec2, - input: &egui::plot::GridInput, -) -> Vec { - let minimum_medium_line_spacing = 150.0; // ≈min size of a label - let max_medium_lines = canvas_size.x as f64 / minimum_medium_line_spacing; - - let (min_ns, max_ns) = input.bounds; - let width_ns = max_ns - min_ns; - - let mut small_spacing_ns = 1; - while width_ns / (next_grid_tick_magnitude_ns(small_spacing_ns) as f64) > max_medium_lines { - small_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns); - } - let medium_spacing_ns = next_grid_tick_magnitude_ns(small_spacing_ns); - let big_spacing_ns = next_grid_tick_magnitude_ns(medium_spacing_ns); - - let mut current_ns = (min_ns.floor() as i64) / small_spacing_ns * small_spacing_ns; - let mut marks = vec![]; - - while current_ns <= max_ns.ceil() as i64 { - let is_big_line = current_ns % big_spacing_ns == 0; - let is_medium_line = current_ns % medium_spacing_ns == 0; - - let step_size = if is_big_line { - big_spacing_ns - } else if is_medium_line { - medium_spacing_ns - } else { - small_spacing_ns - }; - - marks.push(egui::plot::GridMark { - value: current_ns as f64, - step_size: step_size as f64, - }); - - current_ns += small_spacing_ns; - } - - marks -} - -fn round_ns_to_start_of_day(ns: i64) -> i64 { - let ns_per_day = 24 * 60 * 60 * 1_000_000_000; - (ns + ns_per_day / 2) / ns_per_day * ns_per_day -} diff --git a/crates/re_viewport/src/viewport.rs b/crates/re_viewport/src/viewport.rs index 55d7b36fd70e..8008653d208f 100644 --- a/crates/re_viewport/src/viewport.rs +++ b/crates/re_viewport/src/viewport.rs @@ -21,7 +21,6 @@ use crate::{ space_view_entity_picker::SpaceViewEntityPicker, space_view_heuristics::{all_possible_space_views, default_created_space_views}, space_view_highlights::highlights_for_space_view, - view_category::ViewCategory, }; // ---------------------------------------------------------------------------- @@ -489,10 +488,12 @@ impl Viewport { .into_iter() .sorted_by_key(|space_view| space_view.space_origin.to_string()) { - let icon = if let Ok(class) = ctx.space_view_class_registry.get(space_view.class) { + let icon = if let Some(class) = ctx + .space_view_class_registry + .get_or_log_error(space_view.class) + { class.icon() } else { - // TODO(andreas): Error handling if class is not found once categories are gone. space_view.category.icon() }; @@ -556,7 +557,9 @@ impl ViewportState { self.space_view_states .entry(space_view_id) .or_insert_with(|| { - let state = if let Ok(state) = space_view_class_registry.get(space_view_class) { + let state = if let Some(state) = + space_view_class_registry.get_or_log_error(space_view_class) + { state.new_state() } else { // TODO(andreas): Enable this once categories are gone. @@ -569,7 +572,6 @@ impl ViewportState { SpaceViewState { state, selected_tensor: Default::default(), - state_time_series: Default::default(), state_tensors: Default::default(), } }) @@ -832,23 +834,11 @@ fn help_text_ui( space_view_blueprint: &SpaceViewBlueprint, space_view_state: &SpaceViewState, ) { - let help_text = if let Ok(class) = ctx + if let Some(help_text) = ctx .space_view_class_registry - .get(space_view_blueprint.class) + .get_or_log_error(space_view_blueprint.class) + .map(|class| class.help_text(ctx.re_ui, space_view_state.state.as_ref())) { - Some(class.help_text(ctx.re_ui, space_view_state.state.as_ref())) - } else { - match space_view_blueprint.category { - ViewCategory::TimeSeries => Some(crate::view_time_series::help_text(ctx.re_ui)), - ViewCategory::TextBox - | ViewCategory::Text - | ViewCategory::Tensor - | ViewCategory::Spatial - | ViewCategory::BarChart => None, - } - }; - - if let Some(help_text) = help_text { re_ui::help_hover_button(ui).on_hover_text(help_text); } } diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh index 19d787ee0f82..56755e9dc44b 100755 --- a/scripts/publish_crates.sh +++ b/scripts/publish_crates.sh @@ -122,9 +122,10 @@ cargo publish $FLAGS -p re_time_panel cargo publish $FLAGS -p re_space_view cargo publish $FLAGS -p re_viewport cargo publish $FLAGS -p re_space_view_bar_chart +cargo publish $FLAGS -p re_space_view_spatial cargo publish $FLAGS -p re_space_view_text cargo publish $FLAGS -p re_space_view_text_box -cargo publish $FLAGS -p re_space_view_spatial +cargo publish $FLAGS -p re_space_view_time_series cargo publish $FLAGS -p re_viewer cargo publish $FLAGS -p re_sdk cargo publish $FLAGS -p rerun