From a02c262db38456151e7356394dd3549be70bd371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Fri, 13 Dec 2024 09:29:27 +0100 Subject: [PATCH] Improve `VisualBounds2D` behavior in graph view (#8438) * Closes #8429 This implements improved handling of `VisualBounds2D`. Specifically, it: * Always defaults to the data bounds when user has not modified the view (@nikolausWest's suggestion). * Zooms in and out of view when resizing the view (@abey79's suggestion). * Fixes the `VisualBounds2D` selection panel bug (submitted by @Wumpf) --------- Co-authored-by: Andreas Reich --- crates/viewer/re_ui/src/zoom_pan_area.rs | 14 ++++++++++---- crates/viewer/re_view_graph/src/ui/state.rs | 5 ++--- crates/viewer/re_view_graph/src/view.rs | 19 +++++-------------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/viewer/re_ui/src/zoom_pan_area.rs b/crates/viewer/re_ui/src/zoom_pan_area.rs index 46020aa15c34..bfb97c1ef597 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}; +use egui::{emath::TSTransform, Area, Rect, Response, Ui, UiKind, 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) { @@ -19,16 +19,22 @@ fn register_pan_and_zoom(ui: &Ui, resp: &Response, ui_from_scene: &mut TSTransfo let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); + // Most of the time we can return early. This is also important to + // avoid `ui_from_scene` to change slightly due to floating point errors. + if zoom_delta == 1.0 && pan_delta == Vec2::ZERO { + return; + } + // Zoom in on pointer, but only if we are not zoomed out too far. if zoom_delta < 1.0 || ui_from_scene.scaling < 1.0 { *ui_from_scene = *ui_from_scene * TSTransform::from_translation(pointer_in_scene.to_vec2()) * TSTransform::from_scaling(zoom_delta) * TSTransform::from_translation(-pointer_in_scene.to_vec2()); - } - // We clamp the resulting scaling to avoid zooming out too far. - ui_from_scene.scaling = ui_from_scene.scaling.min(1.0); + // We clamp the resulting scaling to avoid zooming out too far. + ui_from_scene.scaling = ui_from_scene.scaling.min(1.0); + } // Pan: *ui_from_scene = TSTransform::from_translation(pan_delta) * *ui_from_scene; diff --git a/crates/viewer/re_view_graph/src/ui/state.rs b/crates/viewer/re_view_graph/src/ui/state.rs index 05eff3718fbc..d8dce815d1df 100644 --- a/crates/viewer/re_view_graph/src/ui/state.rs +++ b/crates/viewer/re_view_graph/src/ui/state.rs @@ -1,4 +1,4 @@ -use egui::{emath::TSTransform, Rect}; +use egui::Rect; use re_format::format_f32; use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; @@ -16,7 +16,6 @@ pub struct GraphViewState { pub show_debug: bool, pub visual_bounds: Option, - pub ui_from_world: Option, pub rect_in_ui: Option, } @@ -25,7 +24,7 @@ impl GraphViewState { let Some(rect) = self.layout_state.bounding_rect() else { return; }; - ui.grid_left_hand_label("Layout") + ui.grid_left_hand_label("Bounding box") .on_hover_text("The bounding box encompassing all entities in the view right now"); ui.vertical(|ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); diff --git a/crates/viewer/re_view_graph/src/view.rs b/crates/viewer/re_view_graph/src/view.rs index 8948a343c423..581dd8bdcec9 100644 --- a/crates/viewer/re_view_graph/src/view.rs +++ b/crates/viewer/re_view_graph/src/view.rs @@ -183,22 +183,14 @@ Display a graph of nodes and edges. let layout = state.layout_state.get(request, params); // Prepare the view and the transformations. - let prev_rect_in_ui = state.rect_in_ui; let rect_in_ui = *state.rect_in_ui.insert(ui.max_rect()); - let ui_from_world = state - .ui_from_world - .get_or_insert_with(|| fit_to_rect_in_scene(rect_in_ui, rect_in_scene.into())); + let mut ui_from_world = fit_to_rect_in_scene(rect_in_ui, rect_in_scene.into()); - // We ensure that the view's center is kept during resizing. - if let Some(prev) = prev_rect_in_ui { - if prev != rect_in_ui { - let delta = rect_in_ui.center() - prev.center(); - ui_from_world.translation += delta; - } - } + // 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, ui_from_world, |ui| { + let resp = zoom_pan_area(ui, rect_in_ui, &mut ui_from_world, |ui| { let mut world_bounding_rect = egui::Rect::NOTHING; for graph in &graphs { @@ -217,8 +209,7 @@ Display a graph of nodes and edges. blueprint::components::VisualBounds2D::from(ui_from_world.inverse() * rect_in_ui); if resp.double_clicked() { bounds_property.reset_blueprint_component::(ctx); - state.ui_from_world = None; - } else if rect_in_scene != updated_rect_in_scene { + } else if ui_from_world != ui_from_world_ref { bounds_property.save_blueprint_component(ctx, &updated_rect_in_scene); } // Update stored bounds on the state, so visualizers see an up-to-date value.