diff --git a/Cargo.lock b/Cargo.lock index ce79578ce525..9fef53190808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1934,7 +1934,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecolor" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "bytemuck", "color-hex", @@ -1951,7 +1951,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f" [[package]] name = "eframe" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "ahash", "bytemuck", @@ -1972,7 +1972,7 @@ dependencies = [ "parking_lot", "percent-encoding", "pollster 0.4.0", - "puffin", + "profiling", "raw-window-handle", "ron", "serde", @@ -1990,7 +1990,7 @@ dependencies = [ [[package]] name = "egui" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "accesskit", "ahash", @@ -1999,7 +1999,7 @@ dependencies = [ "epaint", "log", "nohash-hasher", - "puffin", + "profiling", "ron", "serde", ] @@ -2007,7 +2007,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "ahash", "bytemuck", @@ -2015,7 +2015,7 @@ dependencies = [ "egui", "epaint", "log", - "puffin", + "profiling", "thiserror", "type-map", "web-time", @@ -2026,14 +2026,14 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "accesskit_winit", "ahash", "arboard", "egui", "log", - "puffin", + "profiling", "raw-window-handle", "serde", "smithay-clipboard", @@ -2068,7 +2068,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "ahash", "egui", @@ -2077,7 +2077,7 @@ dependencies = [ "image", "log", "mime_guess2", - "puffin", + "profiling", "resvg", "serde", ] @@ -2085,16 +2085,15 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "ahash", "bytemuck", "egui", - "egui-winit", "glow 0.16.0", "log", "memoffset", - "puffin", + "profiling", "wasm-bindgen", "web-sys", "winit", @@ -2103,7 +2102,7 @@ dependencies = [ [[package]] name = "egui_kittest" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "dify", "egui", @@ -2172,7 +2171,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "bytemuck", "serde", @@ -2288,7 +2287,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" dependencies = [ "ab_glyph", "ahash", @@ -2299,7 +2298,7 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", - "puffin", + "profiling", "rayon", "serde", ] @@ -2307,7 +2306,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" +source = "git+https://github.com/emilk/egui.git?rev=f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b#f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" [[package]] name = "equivalent" diff --git a/Cargo.toml b/Cargo.toml index b77fdec56049..b4b3d7f23011 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,6 @@ ecolor = "0.29.1" eframe = { version = "0.29.1", default-features = false, features = [ "accesskit", "default_fonts", - "puffin", "wayland", "x11", ] } @@ -135,16 +134,10 @@ egui = { version = "0.29.1", features = [ "callstack", "color-hex", "log", - "puffin", "rayon", ] } egui_commonmark = { version = "0.18", default-features = false } -egui_extras = { version = "0.29.1", features = [ - "http", - "image", - "puffin", - "serde", -] } +egui_extras = { version = "0.29.1", features = ["http", "image", "serde"] } egui_kittest = { version = "0.29.1", features = ["wgpu", "snapshot"] } egui_plot = "0.29.0" # https://github.com/emilk/egui_plot egui_table = "0.1.0" # https://github.com/rerun-io/egui_table @@ -561,13 +554,13 @@ significant_drop_tightening = "allow" # An update of parking_lot made this trigg # 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. -ecolor = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 -eframe = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 -egui = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 -egui_extras = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 -egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 -egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 -emath = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09 +ecolor = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 +eframe = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 +egui = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 +egui_extras = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 +egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 +egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 +emath = { git = "https://github.com/emilk/egui.git", rev = "f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b" } # egui master 2024-12-16 # Useful while developing: # ecolor = { path = "../../egui/crates/ecolor" } 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(()) +} 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/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 581dd8bdcec9..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 resp = zoom_pan_area(ui, rect_in_ui, &mut ui_from_world, |ui| { + 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); } diff --git a/crates/viewer/re_viewer/Cargo.toml b/crates/viewer/re_viewer/Cargo.toml index ae496fc924a6..3b076ba1df97 100644 --- a/crates/viewer/re_viewer/Cargo.toml +++ b/crates/viewer/re_viewer/Cargo.toml @@ -104,7 +104,6 @@ cfg-if.workspace = true eframe = { workspace = true, default-features = false, features = [ "default_fonts", "persistence", - "puffin", "wgpu", ] } egui_plot.workspace = true 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)]) 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