From 4ccfcef667816792945c55f5d474f732f9b3f685 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:09:19 +0100 Subject: [PATCH 1/9] Add utility to `rr.components.Color` to generate colors from any string (and use it in the air traffic data example) (#8458) ### What It's some time nice to log some color information in multiple entities to make it easier to relate them visually. This PR adds a `rr.components.Color.from_str()` utility that does exactly that: generate a nice color randomly picked up based on the provided string. This PR also updates the air traffic data example so the barometric traces have matching colors with the map data. --------- Co-authored-by: Clement Rey --- .../air_traffic_data/air_traffic_data.py | 27 ++++++++++++------ rerun_py/rerun_sdk/rerun/components/color.py | 3 +- .../rerun_sdk/rerun/components/color_ext.py | 28 +++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 rerun_py/rerun_sdk/rerun/components/color_ext.py diff --git a/examples/python/air_traffic_data/air_traffic_data.py b/examples/python/air_traffic_data/air_traffic_data.py index 911c4423e69d..2e2352e0ef0c 100644 --- a/examples/python/air_traffic_data/air_traffic_data.py +++ b/examples/python/air_traffic_data/air_traffic_data.py @@ -239,6 +239,7 @@ def process_measurement(self, measurement: Measurement) -> None: ) entity_path = f"aircraft/{measurement.icao_id}" + color = rr.components.Color.from_string(entity_path) if ( measurement.latitude is not None @@ -247,13 +248,16 @@ def process_measurement(self, measurement: Measurement) -> None: ): rr.log( entity_path, - rr.Points3D([ - self._proj.transform( - measurement.longitude, - measurement.latitude, - measurement.barometric_altitude, - ) - ]), + rr.Points3D( + [ + self._proj.transform( + measurement.longitude, + measurement.latitude, + measurement.barometric_altitude, + ), + ], + colors=color, + ), rr.GeoPoints(lat_lon=[measurement.latitude, measurement.longitude]), ) @@ -264,6 +268,7 @@ def process_measurement(self, measurement: Measurement) -> None: rr.log( entity_path + "/barometric_altitude", rr.Scalar(measurement.barometric_altitude), + rr.SeriesLine(color=color), ) def flush(self) -> None: @@ -310,7 +315,13 @@ def log_position_and_altitude(self, df: polars.DataFrame, icao_id: str) -> None: return if icao_id not in self._position_indicators: - rr.log(entity_path, [rr.archetypes.Points3D.indicator(), rr.archetypes.GeoPoints.indicator()], static=True) + color = rr.components.Color.from_string(entity_path) + rr.log( + entity_path, + [rr.archetypes.Points3D.indicator(), rr.archetypes.GeoPoints.indicator(), color], + static=True, + ) + rr.log(entity_path + "/barometric_altitude", [rr.archetypes.SeriesLine.indicator(), color], static=True) self._position_indicators.add(icao_id) timestamps = rr.TimeSecondsColumn("unix_time", df["timestamp"].to_numpy()) diff --git a/rerun_py/rerun_sdk/rerun/components/color.py b/rerun_py/rerun_sdk/rerun/components/color.py index 5d72204fde68..122a0a9cfd7f 100644 --- a/rerun_py/rerun_sdk/rerun/components/color.py +++ b/rerun_py/rerun_sdk/rerun/components/color.py @@ -11,11 +11,12 @@ ComponentDescriptor, ComponentMixin, ) +from .color_ext import ColorExt __all__ = ["Color", "ColorBatch"] -class Color(datatypes.Rgba32, ComponentMixin): +class Color(ColorExt, datatypes.Rgba32, ComponentMixin): """ **Component**: An RGBA color with unmultiplied/separate alpha, in sRGB gamma space with linear alpha. diff --git a/rerun_py/rerun_sdk/rerun/components/color_ext.py b/rerun_py/rerun_sdk/rerun/components/color_ext.py new file mode 100644 index 000000000000..b06d2f7c9624 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/color_ext.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import colorsys +import math +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import Color + +_GOLDEN_RATIO = (math.sqrt(5.0) - 1.0) / 2.0 + + +class ColorExt: + """Extension for [Color][rerun.components.Color].""" + + @staticmethod + def from_string(s: str) -> Color: + """ + Generate a random yet deterministic color based on a string. + + The color is guaranteed to be identical for the same input string. + """ + + from . import Color + + # adapted from egui::PlotUi + hue = (hash(s) & 0xFFFF) / 2**16 * _GOLDEN_RATIO + return Color([round(comp * 255) for comp in colorsys.hsv_to_rgb(hue, 0.85, 0.5)]) From c439c8abb37964c7fc690d84419fa6a67353db34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 16 Dec 2024 10:35:21 +0100 Subject: [PATCH 2/9] =?UTF-8?q?Attempt=20to=20fix=20`StorageNodeClient.que?= =?UTF-8?q?ry=5Fcatalog(=E2=80=A6)`=20signature=20mismatch=20(#8468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Related CI is failing on `nightly`: https://github.com/rerun-io/rerun/actions/runs/12335447188/job/34427482723 ### What Seems to be a mismatch between doc strings. --- rerun_py/src/remote.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rerun_py/src/remote.rs b/rerun_py/src/remote.rs index b8af391dd922..a74f3a1a0666 100644 --- a/rerun_py/src/remote.rs +++ b/rerun_py/src/remote.rs @@ -81,7 +81,7 @@ pub struct PyStorageNodeClient { #[pymethods] impl PyStorageNodeClient { - /// Query the recordings metadata catalog. + /// Get the metadata for all recordings in the storage node. fn query_catalog(&mut self) -> PyResult>> { let reader = self.runtime.block_on(async { // TODO(jleibs): Support column projection and filtering From 8e2cf5f9a03bad498e41eb391e03200223e29af6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 10:35:41 +0100 Subject: [PATCH 3/9] Fix broken interaction outside of graph views (#8457) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Related * Closes #8454 UI interaction was sometimes broken outside of graph views. This PR is the cure. * Reverts https://github.com/rerun-io/rerun/pull/8416 --------- Co-authored-by: Jochen Görtler --- crates/viewer/re_ui/src/zoom_pan_area.rs | 86 +++++++++++++----------- crates/viewer/re_view_graph/src/view.rs | 2 +- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/crates/viewer/re_ui/src/zoom_pan_area.rs b/crates/viewer/re_ui/src/zoom_pan_area.rs index bfb97c1ef597..f3f2530ba571 100644 --- a/crates/viewer/re_ui/src/zoom_pan_area.rs +++ b/crates/viewer/re_ui/src/zoom_pan_area.rs @@ -5,7 +5,7 @@ //! * `view`-space: The space where the pan-and-zoom area is drawn. //! * `scene`-space: The space where the actual content is drawn. -use egui::{emath::TSTransform, Area, Rect, Response, Ui, UiKind, Vec2}; +use egui::{emath::TSTransform, Rect, Response, Ui, UiBuilder, Vec2}; /// Helper function to handle pan and zoom interactions on a response. fn register_pan_and_zoom(ui: &Ui, resp: &Response, ui_from_scene: &mut TSTransform) { @@ -63,46 +63,52 @@ pub fn fit_to_rect_in_scene(rect_in_ui: Rect, rect_in_scene: Rect) -> TSTransfor } /// Provides a zoom-pan area for a given view. +/// +/// Will fill the entire `max_rect` of the `parent_ui`. pub fn zoom_pan_area( - ui: &Ui, - view_bounds_in_ui: Rect, - ui_from_scene: &mut TSTransform, + parent_ui: &mut Ui, + to_global: &mut TSTransform, draw_contents: impl FnOnce(&mut Ui), ) -> Response { - let area_resp = Area::new(ui.id().with("zoom_pan_area")) - .constrain_to(view_bounds_in_ui) - .order(ui.layer_id().order) - .kind(UiKind::GenericArea) - .show(ui.ctx(), |ui| { - // Transform to the scene space: - let visible_rect_in_scene = ui_from_scene.inverse() * view_bounds_in_ui; - - // set proper clip-rect so we can interact with the background. - ui.set_clip_rect(visible_rect_in_scene); - - // A Ui for sensing drag-to-pan, scroll-to-zoom, etc - let mut drag_sense_ui = ui.new_child( - egui::UiBuilder::new() - .sense(egui::Sense::click_and_drag()) - .max_rect(visible_rect_in_scene), - ); - - drag_sense_ui.set_min_size(visible_rect_in_scene.size()); - let pan_response = drag_sense_ui.response(); - - // Update the transform based on the interactions: - register_pan_and_zoom(ui, &pan_response, ui_from_scene); - - // Update the clip-rect with the new transform, to avoid frame-delays - ui.set_clip_rect(ui_from_scene.inverse() * view_bounds_in_ui); - - // Add the actual contents to the area: - draw_contents(ui); - pan_response - }); - - ui.ctx() - .set_transform_layer(area_resp.response.layer_id, *ui_from_scene); - - area_resp.inner + // Create a new egui paint layer, where we can draw our contents: + let zoom_pan_layer_id = egui::LayerId::new( + parent_ui.layer_id().order, + parent_ui.id().with("zoom_pan_area"), + ); + + // Put the layer directly on-top of the main layer of the ui: + parent_ui + .ctx() + .set_sublayer(parent_ui.layer_id(), zoom_pan_layer_id); + + let global_view_bounds = parent_ui.max_rect(); + + let mut local_ui = parent_ui.new_child( + UiBuilder::new() + .layer_id(zoom_pan_layer_id) + .max_rect(to_global.inverse() * global_view_bounds) + .sense(egui::Sense::click_and_drag()), + ); + local_ui.set_min_size(local_ui.max_rect().size()); // Allocate all available space + + // Set proper clip-rect so we can interact with the background: + local_ui.set_clip_rect(local_ui.max_rect()); + + let pan_response = local_ui.response(); + + // Update the `to_global` transform based on use interaction: + register_pan_and_zoom(&local_ui, &pan_response, to_global); + + // Update the clip-rect with the new transform, to avoid frame-delays + local_ui.set_clip_rect(to_global.inverse() * global_view_bounds); + + // Add the actual contents to the area: + draw_contents(&mut local_ui); + + // Tell egui to apply the transform on the layer: + local_ui + .ctx() + .set_transform_layer(zoom_pan_layer_id, *to_global); + + pan_response } diff --git a/crates/viewer/re_view_graph/src/view.rs b/crates/viewer/re_view_graph/src/view.rs index 581dd8bdcec9..edb59eceb27d 100644 --- a/crates/viewer/re_view_graph/src/view.rs +++ b/crates/viewer/re_view_graph/src/view.rs @@ -190,7 +190,7 @@ Display a graph of nodes and edges. // We store a copy of the transformation to see if it has changed. let ui_from_world_ref = ui_from_world; - let resp = zoom_pan_area(ui, rect_in_ui, &mut ui_from_world, |ui| { + let resp = zoom_pan_area(ui, &mut ui_from_world, |ui| { let mut world_bounding_rect = egui::Rect::NOTHING; for graph in &graphs { From 745e6f8d48365c3557d5d6eee0cf66cb00281d4b Mon Sep 17 00:00:00 2001 From: Zeljko Mihaljcic <7150613+zehiko@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:28:15 +0100 Subject: [PATCH 4/9] craft blueprint for Catalog view from raw chunks (#8449) --- crates/store/re_grpc_client/src/lib.rs | 137 +++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 7 deletions(-) diff --git a/crates/store/re_grpc_client/src/lib.rs b/crates/store/re_grpc_client/src/lib.rs index b37c07f20137..5b6f553ee428 100644 --- a/crates/store/re_grpc_client/src/lib.rs +++ b/crates/store/re_grpc_client/src/lib.rs @@ -5,8 +5,12 @@ mod address; pub use address::{InvalidRedapAddress, RedapAddress}; use re_chunk::external::arrow2; use re_log_types::external::re_types_core::ComponentDescriptor; +use re_types::blueprint::archetypes::{ContainerBlueprint, ViewportBlueprint}; +use re_types::blueprint::archetypes::{ViewBlueprint, ViewContents}; +use re_types::blueprint::components::{ContainerKind, RootContainer}; use re_types::components::RecordingUri; -use re_types::Component; +use re_types::external::uuid; +use re_types::{Archetype, Component}; use url::Url; // ---------------------------------------------------------------------------- @@ -15,10 +19,13 @@ use std::error::Error; use arrow2::array::Utf8Array as Arrow2Utf8Array; use arrow2::datatypes::Field as Arrow2Field; -use re_chunk::{Arrow2Array, Chunk, ChunkId, TransportChunk}; +use re_chunk::{ + Arrow2Array, Chunk, ChunkBuilder, ChunkId, EntityPath, RowId, Timeline, TransportChunk, +}; use re_log_encoding::codec::{wire::decode, CodecError}; use re_log_types::{ - ApplicationId, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind, StoreSource, Time, + ApplicationId, BlueprintActivationCommand, EntityPathFilter, LogMsg, SetStoreInfo, StoreId, + StoreInfo, StoreKind, StoreSource, Time, }; use re_protos::common::v0::RecordingId; use re_protos::remote_store::v0::{ @@ -77,6 +84,10 @@ enum StreamError { // ---------------------------------------------------------------------------- +const CATALOG_BP_STORE_ID: &str = "catalog_blueprint"; +const CATALOG_REC_STORE_ID: &str = "catalog"; +const CATALOG_APPLICATION_ID: &str = "redap_catalog"; + /// Stream an rrd file or metadsasta catalog over gRPC from a Rerun Data Platform server. /// /// `on_msg` can be used to wake up the UI thread on Wasm. @@ -276,11 +287,16 @@ async fn stream_catalog_async( drop(client); - // We need a whole StoreInfo here. - let store_id = StoreId::from_string(StoreKind::Recording, "catalog".to_owned()); + if activate_catalog_blueprint(&tx).is_err() { + re_log::debug!("Failed to activate catalog blueprint"); + return Ok(()); + } + + // Craft the StoreInfo for the actual catalog data + let store_id = StoreId::from_string(StoreKind::Recording, CATALOG_REC_STORE_ID.to_owned()); let store_info = StoreInfo { - application_id: ApplicationId::from("redap_catalog"), + application_id: ApplicationId::from(CATALOG_APPLICATION_ID), store_id: store_id.clone(), cloned_from: None, is_official_example: false, @@ -309,7 +325,6 @@ async fn stream_catalog_async( TransportChunk::CHUNK_METADATA_KEY_ID.to_owned(), ChunkId::new().to_string(), ); - let mut chunk = Chunk::from_transport(&tc)?; // enrich catalog data with RecordingUri that's based on the ReDap endpoint (that we know) @@ -376,3 +391,111 @@ async fn stream_catalog_async( Ok(()) } + +// Craft a blueprint from relevant chunks and activate it +// TODO(zehiko) - manual crafting of the blueprint as we have below will go away and be replaced +// by either a blueprint crafted using rust Blueprint API or a blueprint fetched from ReDap (#8470) +fn activate_catalog_blueprint( + tx: &re_smart_channel::Sender, +) -> Result<(), Box> { + let blueprint_store_id = + StoreId::from_string(StoreKind::Blueprint, CATALOG_BP_STORE_ID.to_owned()); + let blueprint_store_info = StoreInfo { + application_id: ApplicationId::from(CATALOG_APPLICATION_ID), + store_id: blueprint_store_id.clone(), + cloned_from: None, + is_official_example: false, + started: Time::now(), + store_source: StoreSource::Unknown, + store_version: None, + }; + + if tx + .send(LogMsg::SetStoreInfo(SetStoreInfo { + row_id: *re_chunk::RowId::new(), + info: blueprint_store_info, + })) + .is_err() + { + re_log::debug!("Receiver disconnected"); + return Ok(()); + } + + let timepoint = [(Timeline::new_sequence("blueprint"), 1)]; + + let vb = ViewBlueprint::new("Dataframe") + .with_visible(true) + .with_space_origin("/"); + + // TODO(zehiko) we shouldn't really be creating all these ids and entity paths manually... (#8470) + let view_uuid = uuid::Uuid::new_v4(); + let view_entity_path = format!("/view/{view_uuid}"); + let view_chunk = ChunkBuilder::new(ChunkId::new(), view_entity_path.clone().into()) + .with_archetype(RowId::new(), timepoint, &vb) + .build()?; + + let epf = EntityPathFilter::parse_forgiving("/**", &Default::default()); + let vc = ViewContents::new(epf.iter_expressions()); + let view_contents_chunk = ChunkBuilder::new( + ChunkId::new(), + format!( + "{}/{}", + view_entity_path.clone(), + ViewContents::name().short_name() + ) + .into(), + ) + .with_archetype(RowId::new(), timepoint, &vc) + .build()?; + + let rc = ContainerBlueprint::new(ContainerKind::Grid) + .with_contents(&[EntityPath::from(view_entity_path)]) + .with_visible(true); + + let container_uuid = uuid::Uuid::new_v4(); + let container_chunk = ChunkBuilder::new( + ChunkId::new(), + format!("/container/{container_uuid}").into(), + ) + .with_archetype(RowId::new(), timepoint, &rc) + .build()?; + + let vp = ViewportBlueprint::new().with_root_container(RootContainer(container_uuid.into())); + let viewport_chunk = ChunkBuilder::new(ChunkId::new(), "/viewport".into()) + .with_archetype(RowId::new(), timepoint, &vp) + .build()?; + + for chunk in &[ + view_chunk, + view_contents_chunk, + container_chunk, + viewport_chunk, + ] { + if tx + .send(LogMsg::ArrowMsg( + blueprint_store_id.clone(), + chunk.to_arrow_msg()?, + )) + .is_err() + { + re_log::debug!("Receiver disconnected"); + return Ok(()); + } + } + + let blueprint_activation = BlueprintActivationCommand { + blueprint_id: blueprint_store_id.clone(), + make_active: true, + make_default: true, + }; + + if tx + .send(LogMsg::BlueprintActivationCommand(blueprint_activation)) + .is_err() + { + re_log::debug!("Receiver disconnected"); + return Ok(()); + } + + Ok(()) +} From dd77abac475dc59d3c72f0c1bc7dd7d029b060d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 16 Dec 2024 11:53:08 +0100 Subject: [PATCH 5/9] Implement level-of-detail on text labels (#8450) ### What @emilk and I discussed level-of-details on the graph. Here is an initial experiment. Overall, I'm not sure the performance improvements warrant this addition. Discussion + video: https://rerunio.slack.com/archives/C041NHU952S/p1734084741247629 --------- Co-authored-by: Antoine Beyeler <49431240+abey79@users.noreply.github.com> --- crates/viewer/re_view_graph/src/ui/draw.rs | 76 ++++++++++++++++++++-- crates/viewer/re_view_graph/src/ui/mod.rs | 2 +- crates/viewer/re_view_graph/src/view.rs | 6 +- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/crates/viewer/re_view_graph/src/ui/draw.rs b/crates/viewer/re_view_graph/src/ui/draw.rs index 0916e1026372..2b226f4041ca 100644 --- a/crates/viewer/re_view_graph/src/ui/draw.rs +++ b/crates/viewer/re_view_graph/src/ui/draw.rs @@ -35,6 +35,7 @@ impl DrawableLabel { } pub struct TextLabel { + color: Option, frame: Frame, galley: Arc, } @@ -44,11 +45,27 @@ pub struct CircleLabel { color: Option, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LevelOfDetail { + Full, + Low, +} + +impl LevelOfDetail { + pub fn from_scaling(zoom: f32) -> Self { + if zoom < 0.20 { + Self::Low + } else { + Self::Full + } + } +} + impl DrawableLabel { pub fn size(&self) -> Vec2 { match self { Self::Circle(CircleLabel { radius, .. }) => Vec2::splat(radius * 2.0), - Self::Text(TextLabel { galley, frame }) => { + Self::Text(TextLabel { galley, frame, .. }) => { frame.inner_margin.sum() + galley.size() + Vec2::splat(frame.stroke.width * 2.0) } } @@ -82,7 +99,11 @@ impl DrawableLabel { .fill(ui.style().visuals.widgets.noninteractive.bg_fill) .stroke(Stroke::new(1.0, ui.style().visuals.text_color())); - Self::Text(TextLabel { frame, galley }) + Self::Text(TextLabel { + frame, + galley, + color, + }) } } @@ -115,7 +136,7 @@ fn draw_circle_label( } fn draw_text_label(ui: &mut Ui, label: &TextLabel, highlight: InteractionHighlight) -> Response { - let TextLabel { galley, frame } = label; + let TextLabel { galley, frame, .. } = label; let visuals = &ui.style().visuals; let bg = match highlight.hover { @@ -137,12 +158,48 @@ fn draw_text_label(ui: &mut Ui, label: &TextLabel, highlight: InteractionHighlig .inner } +/// Draw a rectangle to "fake" a label at small scales, where actual text would be unreadable anyways. +fn draw_rect_label(ui: &mut Ui, label: &TextLabel, highlight: InteractionHighlight) -> Response { + let TextLabel { + galley, + frame, + color, + } = label; + let visuals = ui.visuals(); + + let bg = match highlight.hover { + HoverHighlight::None => visuals.widgets.noninteractive.bg_fill, + HoverHighlight::Hovered => visuals.widgets.hovered.bg_fill, + }; + + let stroke = match highlight.selection { + SelectionHighlight::Selection => visuals.selection.stroke, + _ => Stroke::new(1.0, visuals.text_color()), + }; + + // We use `gamma` to correct for the fact that text is not completely solid. + let fill_color = color + .unwrap_or_else(|| visuals.text_color()) + .gamma_multiply(0.5); + + frame + .stroke(stroke) + .fill(bg) + .show(ui, |ui| { + let (resp, painter) = ui.allocate_painter(galley.rect.size(), Sense::click()); + painter.rect_filled(resp.rect, 0.0, fill_color); + resp + }) + .inner +} + /// Draws a node at the given position. fn draw_node( ui: &mut Ui, center: Pos2, node: &DrawableLabel, highlight: InteractionHighlight, + lod: LevelOfDetail, ) -> Response { let builder = UiBuilder::new() .max_rect(Rect::from_center_size(center, node.size())) @@ -152,7 +209,13 @@ fn draw_node( match node { DrawableLabel::Circle(label) => draw_circle_label(&mut node_ui, label, highlight), - DrawableLabel::Text(label) => draw_text_label(&mut node_ui, label, highlight), + DrawableLabel::Text(label) => { + if lod == LevelOfDetail::Full { + draw_text_label(&mut node_ui, label, highlight) + } else { + draw_rect_label(&mut node_ui, label, highlight) + } + } }; node_ui.response() @@ -281,6 +344,7 @@ pub fn draw_graph( graph: &Graph, layout: &Layout, query: &ViewQuery<'_>, + lod: LevelOfDetail, ) -> Rect { let entity_path = graph.entity(); let entity_highlights = query.highlights.entity_highlight(entity_path.hash()); @@ -294,7 +358,7 @@ pub fn draw_graph( let response = match node { Node::Explicit { instance, .. } => { let highlight = entity_highlights.index_highlight(instance.instance_index); - let mut response = draw_node(ui, center, node.label(), highlight); + let mut response = draw_node(ui, center, node.label(), highlight, lod); let instance_path = InstancePath::instance(entity_path.clone(), instance.instance_index); @@ -322,7 +386,7 @@ pub fn draw_graph( response } Node::Implicit { graph_node, .. } => { - draw_node(ui, center, node.label(), Default::default()).on_hover_text(format!( + draw_node(ui, center, node.label(), Default::default(), lod).on_hover_text(format!( "Implicit node {} created via a reference in a GraphEdge component", graph_node.as_str(), )) diff --git a/crates/viewer/re_view_graph/src/ui/mod.rs b/crates/viewer/re_view_graph/src/ui/mod.rs index 4e00cdfe7388..ef5e09d555fa 100644 --- a/crates/viewer/re_view_graph/src/ui/mod.rs +++ b/crates/viewer/re_view_graph/src/ui/mod.rs @@ -2,6 +2,6 @@ mod draw; mod selection; mod state; -pub use draw::{draw_debug, draw_graph, DrawableLabel}; +pub use draw::{draw_debug, draw_graph, DrawableLabel, LevelOfDetail}; pub use selection::view_property_force_ui; pub use state::GraphViewState; diff --git a/crates/viewer/re_view_graph/src/view.rs b/crates/viewer/re_view_graph/src/view.rs index edb59eceb27d..b2dfdc54d7db 100644 --- a/crates/viewer/re_view_graph/src/view.rs +++ b/crates/viewer/re_view_graph/src/view.rs @@ -28,7 +28,7 @@ use re_viewport_blueprint::ViewProperty; use crate::{ graph::Graph, layout::{ForceLayoutParams, LayoutRequest}, - ui::{draw_debug, draw_graph, view_property_force_ui, GraphViewState}, + ui::{draw_debug, draw_graph, view_property_force_ui, GraphViewState, LevelOfDetail}, visualizers::{merge, EdgesVisualizer, NodeVisualizer}, }; @@ -190,11 +190,13 @@ Display a graph of nodes and edges. // We store a copy of the transformation to see if it has changed. let ui_from_world_ref = ui_from_world; + let level_of_detail = LevelOfDetail::from_scaling(ui_from_world.scaling); + let resp = zoom_pan_area(ui, &mut ui_from_world, |ui| { let mut world_bounding_rect = egui::Rect::NOTHING; for graph in &graphs { - let graph_rect = draw_graph(ui, ctx, graph, layout, query); + let graph_rect = draw_graph(ui, ctx, graph, layout, query, level_of_detail); world_bounding_rect = world_bounding_rect.union(graph_rect); } From a8ae6f5a770dcd1ae4c2561c7c16edc25b1fd5a4 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Mon, 16 Dec 2024 14:21:26 +0100 Subject: [PATCH 6/9] Fix `re_renderer`'s wgpu error scopes not handling internal errors (#8473) Wasn't exposed in earlier wgpu versions. Usually not a problem, but when it is it's usually too late ;) --- .../src/error_handling/wgpu_error_scope.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/viewer/re_renderer/src/error_handling/wgpu_error_scope.rs b/crates/viewer/re_renderer/src/error_handling/wgpu_error_scope.rs index e9fdaf29b63b..293ae6900706 100644 --- a/crates/viewer/re_renderer/src/error_handling/wgpu_error_scope.rs +++ b/crates/viewer/re_renderer/src/error_handling/wgpu_error_scope.rs @@ -14,7 +14,7 @@ impl WgpuErrorScope { pub fn start(device: &Arc) -> Self { device.push_error_scope(wgpu::ErrorFilter::Validation); device.push_error_scope(wgpu::ErrorFilter::OutOfMemory); - // TODO(gfx-rs/wgpu#4866): Internal is missing! + device.push_error_scope(wgpu::ErrorFilter::Internal); Self { device: device.clone(), open: true, @@ -23,9 +23,13 @@ impl WgpuErrorScope { pub fn end( mut self, - ) -> [impl std::future::Future> + Send + 'static; 2] { + ) -> [impl std::future::Future> + Send + 'static; 3] { self.open = false; - [self.device.pop_error_scope(), self.device.pop_error_scope()] + [ + self.device.pop_error_scope(), + self.device.pop_error_scope(), + self.device.pop_error_scope(), + ] } } @@ -34,6 +38,7 @@ impl Drop for WgpuErrorScope { if self.open { drop(self.device.pop_error_scope()); drop(self.device.pop_error_scope()); + drop(self.device.pop_error_scope()); } } } From 40338bd3c0cbf294f26bb62e557bd2782dfdb19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 16 Dec 2024 14:35:57 +0100 Subject: [PATCH 7/9] Fix single-click and double-click in the graph view (#8474) ### Related * Closes #8437 * Closes #8442 ### What This implements: * Single-click on empty space to select view. * Double-click on node to select entire entity. Merging @emilk's recent changes (#8469 and #8457) seems to have fixed the flickering on selection too. --- crates/viewer/re_view_graph/src/ui/draw.rs | 20 +++++++++++++++----- crates/viewer/re_view_graph/src/view.rs | 8 +++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/viewer/re_view_graph/src/ui/draw.rs b/crates/viewer/re_view_graph/src/ui/draw.rs index 2b226f4041ca..70d3e03b9f17 100644 --- a/crates/viewer/re_view_graph/src/ui/draw.rs +++ b/crates/viewer/re_view_graph/src/ui/draw.rs @@ -362,11 +362,6 @@ pub fn draw_graph( let instance_path = InstancePath::instance(entity_path.clone(), instance.instance_index); - ctx.handle_select_hover_drag_interactions( - &response, - Item::DataResult(query.view_id, instance_path.clone()), - false, - ); response = response.on_hover_ui_at_pointer(|ui| { list_item::list_item_scope(ui, "graph_node_hover", |ui| { @@ -383,6 +378,21 @@ pub fn draw_graph( }); }); + ctx.handle_select_hover_drag_interactions( + &response, + Item::DataResult(query.view_id, instance_path.clone()), + false, + ); + + // double click selects the entire entity + if response.double_clicked() { + // Select the entire entity + ctx.selection_state().set_selection(Item::DataResult( + query.view_id, + instance_path.entity_path.clone().into(), + )); + } + response } Node::Implicit { graph_node, .. } => { diff --git a/crates/viewer/re_view_graph/src/view.rs b/crates/viewer/re_view_graph/src/view.rs index b2dfdc54d7db..b3227406cd51 100644 --- a/crates/viewer/re_view_graph/src/view.rs +++ b/crates/viewer/re_view_graph/src/view.rs @@ -19,7 +19,7 @@ use re_view::{ view_property_ui, }; use re_viewer_context::{ - IdentifiedViewSystem as _, RecommendedView, SystemExecutionOutput, ViewClass, + IdentifiedViewSystem as _, Item, RecommendedView, SystemExecutionOutput, ViewClass, ViewClassLayoutPriority, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics, ViewState, ViewStateExt as _, ViewSystemExecutionError, ViewSystemRegistrator, ViewerContext, }; @@ -206,6 +206,12 @@ Display a graph of nodes and edges. } }); + if resp.clicked() { + // clicked elsewhere, select the view + ctx.selection_state() + .set_selection(Item::View(query.view_id)); + } + // Update blueprint if changed let updated_rect_in_scene = blueprint::components::VisualBounds2D::from(ui_from_world.inverse() * rect_in_ui); From 69fbaa5a0de9e1a0c73597246153071ebb91aaaa Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 14:38:00 +0100 Subject: [PATCH 8/9] Remove wait-time when opening settings panel (#8464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Related * Closes https://github.com/rerun-io/rerun/issues/8263 ### What I couldn't stand the half-second delay when opening the options screen. Rerun needs to feel snappy! Gettings the ffmpeg version is now done on a background thread, showing s spinner until it is done. Unfortunately we still have to wait for ffmpeg when starting up an H.264 video on native. We could fix that by pre-warming the cache though 🤔 --------- Co-authored-by: Clement Rey Co-authored-by: Andreas Reich --- Cargo.lock | 1 + crates/utils/re_video/Cargo.toml | 1 + .../re_video/src/decode/ffmpeg_h264/ffmpeg.rs | 2 +- .../src/decode/ffmpeg_h264/version.rs | 143 +++++++++++------- .../re_viewer/src/ui/settings_screen.rs | 13 +- 5 files changed, 103 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86e23cf1341b..f119af130c21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6511,6 +6511,7 @@ dependencies = [ "js-sys", "once_cell", "parking_lot", + "poll-promise", "re_build_info", "re_build_tools", "re_log", diff --git a/crates/utils/re_video/Cargo.toml b/crates/utils/re_video/Cargo.toml index 2188bdb102ed..890c7441faf9 100644 --- a/crates/utils/re_video/Cargo.toml +++ b/crates/utils/re_video/Cargo.toml @@ -53,6 +53,7 @@ econtext.workspace = true itertools.workspace = true once_cell.workspace = true parking_lot.workspace = true +poll-promise.workspace = true re_mp4.workspace = true thiserror.workspace = true diff --git a/crates/utils/re_video/src/decode/ffmpeg_h264/ffmpeg.rs b/crates/utils/re_video/src/decode/ffmpeg_h264/ffmpeg.rs index 3f8775ab8005..d48f647aa979 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_h264/ffmpeg.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_h264/ffmpeg.rs @@ -811,7 +811,7 @@ impl FFmpegCliH264Decoder { // Check the version once ahead of running FFmpeg. // The error is still handled if it happens while running FFmpeg, but it's a bit unclear if we can get it to start in the first place then. - match FFmpegVersion::for_executable(ffmpeg_path.as_deref()) { + match FFmpegVersion::for_executable_blocking(ffmpeg_path.as_deref()) { Ok(version) => { if !version.is_compatible() { return Err(Error::UnsupportedFFmpegVersion { diff --git a/crates/utils/re_video/src/decode/ffmpeg_h264/version.rs b/crates/utils/re_video/src/decode/ffmpeg_h264/version.rs index a23feaaa14e2..5842108d2fac 100644 --- a/crates/utils/re_video/src/decode/ffmpeg_h264/version.rs +++ b/crates/utils/re_video/src/decode/ffmpeg_h264/version.rs @@ -1,7 +1,8 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf, task::Poll}; use once_cell::sync::Lazy; use parking_lot::Mutex; +use poll_promise::Promise; // FFmpeg 5.1 "Riemann" is from 2022-07-22. // It's simply the oldest I tested manually as of writing. We might be able to go lower. @@ -9,6 +10,8 @@ use parking_lot::Mutex; pub const FFMPEG_MINIMUM_VERSION_MAJOR: u32 = 5; pub const FFMPEG_MINIMUM_VERSION_MINOR: u32 = 1; +pub type FfmpegVersionResult = Result; + /// A successfully parsed `FFmpeg` version. #[derive(Clone, Debug, PartialEq, Eq)] pub struct FFmpegVersion { @@ -78,61 +81,31 @@ impl FFmpegVersion { /// version string. Since version strings can get pretty wild, we don't want to fail in this case. /// /// Internally caches the result per path together with its modification time to re-run/parse the version only if the file has changed. - pub fn for_executable(path: Option<&std::path::Path>) -> Result { - type VersionMap = HashMap< - PathBuf, - ( - Option, - Result, - ), - >; - static CACHE: Lazy> = Lazy::new(|| Mutex::new(HashMap::new())); - + pub fn for_executable_poll(path: Option<&std::path::Path>) -> Poll { re_tracing::profile_function!(); - // Retrieve file modification time first. - let modification_time = if let Some(path) = path { - path.metadata() - .map_err(|err| { - FFmpegVersionParseError::RetrieveFileModificationTime(err.to_string()) - })? - .modified() - .ok() - } else { - None - }; - - // Check first if we already have the version cached. - let mut cache = CACHE.lock(); - let cache_key = path.unwrap_or(std::path::Path::new("ffmpeg")); - if let Some(cached) = cache.get(cache_key) { - if modification_time == cached.0 { - return cached.1.clone(); - } - } - - // Run FFmpeg (or whatever was passed to us) to get the version. - let raw_version = if let Some(path) = path { - ffmpeg_sidecar::version::ffmpeg_version_with_path(path) - } else { - ffmpeg_sidecar::version::ffmpeg_version() - } - .map_err(|err| FFmpegVersionParseError::RunFFmpeg(err.to_string()))?; - - let version = if let Some(version) = Self::parse(&raw_version) { - Ok(version) - } else { - Err(FFmpegVersionParseError::ParseVersion { - raw_version: raw_version.clone(), - }) - }; + let modification_time = file_modification_time(path)?; + VersionCache::global(|cache| { + cache + .version(path, modification_time) + .poll() + .map(|r| r.clone()) + }) + } - cache.insert( - cache_key.to_path_buf(), - (modification_time, version.clone()), - ); + /// Like [`Self::for_executable_poll`], but blocks until the version is ready. + /// + /// WARNING: this blocks for half a second on Mac the first time this is called with a given path, maybe more on other platforms. + pub fn for_executable_blocking(path: Option<&std::path::Path>) -> FfmpegVersionResult { + re_tracing::profile_function!(); - version + let modification_time = file_modification_time(path)?; + VersionCache::global(|cache| { + cache + .version(path, modification_time) + .block_until_ready() + .clone() + }) } /// Returns true if this version is compatible with Rerun's minimum requirements. @@ -143,6 +116,72 @@ impl FFmpegVersion { } } +fn file_modification_time( + path: Option<&std::path::Path>, +) -> Result, FFmpegVersionParseError> { + Ok(if let Some(path) = path { + path.metadata() + .map_err(|err| FFmpegVersionParseError::RetrieveFileModificationTime(err.to_string()))? + .modified() + .ok() + } else { + None + }) +} + +#[derive(Default)] +struct VersionCache( + HashMap, Promise)>, +); + +impl VersionCache { + fn global(f: impl FnOnce(&mut Self) -> R) -> R { + static CACHE: Lazy> = Lazy::new(|| Mutex::new(VersionCache::default())); + f(&mut CACHE.lock()) + } + + fn version( + &mut self, + path: Option<&std::path::Path>, + modification_time: Option, + ) -> &Promise { + let Self(cache) = self; + + let cache_key = path.unwrap_or(std::path::Path::new("ffmpeg")).to_path_buf(); + + match cache.entry(cache_key) { + std::collections::hash_map::Entry::Occupied(entry) => &entry.into_mut().1, + std::collections::hash_map::Entry::Vacant(entry) => { + let path = path.map(|path| path.to_path_buf()); + let version = + Promise::spawn_thread("ffmpeg_version", move || ffmpeg_version(path.as_ref())); + &entry.insert((modification_time, version)).1 + } + } + } +} + +fn ffmpeg_version( + path: Option<&std::path::PathBuf>, +) -> Result { + re_tracing::profile_function!("ffmpeg_version_with_path"); + + let raw_version = if let Some(path) = path { + ffmpeg_sidecar::version::ffmpeg_version_with_path(path) + } else { + ffmpeg_sidecar::version::ffmpeg_version() + } + .map_err(|err| FFmpegVersionParseError::RunFFmpeg(err.to_string()))?; + + if let Some(version) = FFmpegVersion::parse(&raw_version) { + Ok(version) + } else { + Err(FFmpegVersionParseError::ParseVersion { + raw_version: raw_version.clone(), + }) + } +} + #[cfg(test)] mod tests { use crate::decode::ffmpeg_h264::FFmpegVersion; diff --git a/crates/viewer/re_viewer/src/ui/settings_screen.rs b/crates/viewer/re_viewer/src/ui/settings_screen.rs index 2a0aa8b88cf1..21d57618d148 100644 --- a/crates/viewer/re_viewer/src/ui/settings_screen.rs +++ b/crates/viewer/re_viewer/src/ui/settings_screen.rs @@ -203,6 +203,7 @@ fn video_section_ui(ui: &mut Ui, app_options: &mut AppOptions) { #[cfg(not(target_arch = "wasm32"))] fn ffmpeg_path_status_ui(ui: &mut Ui, app_options: &AppOptions) { use re_video::decode::{FFmpegVersion, FFmpegVersionParseError}; + use std::task::Poll; let path = app_options .video_decoder_override_ffmpeg_path @@ -211,17 +212,21 @@ fn ffmpeg_path_status_ui(ui: &mut Ui, app_options: &AppOptions) { if path.is_some_and(|path| !path.is_file()) { ui.error_label("The specified FFmpeg binary path does not exist or is not a file."); } else { - let res = FFmpegVersion::for_executable(path); + let res = FFmpegVersion::for_executable_poll(path); match res { - Ok(version) => { + Poll::Pending => { + ui.spinner(); + } + + Poll::Ready(Ok(version)) => { if version.is_compatible() { ui.success_label(format!("FFmpeg found (version {version})")); } else { ui.error_label(format!("Incompatible FFmpeg version: {version}")); } } - Err(FFmpegVersionParseError::ParseVersion { raw_version }) => { + Poll::Ready(Err(FFmpegVersionParseError::ParseVersion { raw_version })) => { // We make this one a warning instead of an error because version parsing is flaky, and // it might end up still working. ui.warning_label(format!( @@ -229,7 +234,7 @@ fn ffmpeg_path_status_ui(ui: &mut Ui, app_options: &AppOptions) { )); } - Err(err) => { + Poll::Ready(Err(err)) => { ui.error_label(format!("Unable to check FFmpeg version: {err}")); } } From c385877f12b3221a45f163e51cc2bb78b863d3ab Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 14:55:11 +0100 Subject: [PATCH 9/9] Update to re_arrow2 0.18 (#8480) --- Cargo.lock | 5 +++-- Cargo.toml | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f119af130c21..1dba7ac4c891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5527,8 +5527,9 @@ dependencies = [ [[package]] name = "re_arrow2" -version = "0.17.6" -source = "git+https://github.com/rerun-io/re_arrow2?rev=79ba149e823c88c1baa770a33c6ea8b6244d0597#79ba149e823c88c1baa770a33c6ea8b6244d0597" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f046c5679b0f305d610f80d93fd51ad702cfc077bbe16d9553a1660a2505160" dependencies = [ "ahash", "arrow-array", diff --git a/Cargo.toml b/Cargo.toml index f66d51928ef2..da9eb50051ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,7 +152,7 @@ arboard = { version = "3.2", default-features = false } argh = "0.1.12" array-init = "2.1" arrow = { version = "53.1", default-features = false } -arrow2 = { package = "re_arrow2", version = "0.17" } +arrow2 = { package = "re_arrow2", version = "0.18" } async-executor = "1.0" backtrace = "0.3" bincode = "1.3" @@ -580,8 +580,4 @@ egui_tiles = { git = "https://github.com/rerun-io/egui_tiles", rev = "48e0ef5664 # walkers = { git = "https://github.com/rerun-io/walkers", rev = "8939cceb3fa49ca8648ee16fe1d8432f5ab0bdcc" } # https://github.com/podusowski/walkers/pull/222 -# commit on `rerun-io/re_arrow2` `main` branch -# https://github.com/rerun-io/re_arrow2/commit/79ba149e823c88c1baa770a33c6ea8b6244d0597 -re_arrow2 = { git = "https://github.com/rerun-io/re_arrow2", rev = "79ba149e823c88c1baa770a33c6ea8b6244d0597" } - # dav1d = { path = "/home/cmc/dev/rerun-io/rav1d", package = "re_rav1d", version = "0.1.1" }