From 226804ccd6e5447cdb721c2c8c4e30e0295b28fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Wed, 27 Nov 2024 15:52:05 +0100 Subject: [PATCH] [skip ci] Redo graph viewer --- .../viewer/re_space_view_graph/src/canvas.rs | 116 ------------------ crates/viewer/re_space_view_graph/src/draw.rs | 83 +++++++++++++ .../re_space_view_graph/src/graph/mod.rs | 4 +- .../re_space_view_graph/src/layout/layout.rs | 34 +++-- .../src/layout/provider.rs | 108 ++++++++++++---- .../re_space_view_graph/src/layout/request.rs | 14 +-- crates/viewer/re_space_view_graph/src/lib.rs | 2 +- .../re_space_view_graph/src/ui/draw/edge.rs | 67 ---------- .../re_space_view_graph/src/ui/draw/mod.rs | 4 +- .../re_space_view_graph/src/ui/draw/node.rs | 6 +- .../viewer/re_space_view_graph/src/ui/mod.rs | 2 +- .../re_space_view_graph/src/ui/state.rs | 51 +++----- crates/viewer/re_space_view_graph/src/view.rs | 55 ++++----- .../src/visualizers/mod.rs | 2 +- crates/viewer/re_ui/src/lib.rs | 1 + crates/viewer/re_ui/src/zoom_pan_area.rs | 97 +++++++++++++++ 16 files changed, 335 insertions(+), 311 deletions(-) delete mode 100644 crates/viewer/re_space_view_graph/src/canvas.rs create mode 100644 crates/viewer/re_space_view_graph/src/draw.rs delete mode 100644 crates/viewer/re_space_view_graph/src/ui/draw/edge.rs create mode 100644 crates/viewer/re_ui/src/zoom_pan_area.rs diff --git a/crates/viewer/re_space_view_graph/src/canvas.rs b/crates/viewer/re_space_view_graph/src/canvas.rs deleted file mode 100644 index 73a838dd9caf..000000000000 --- a/crates/viewer/re_space_view_graph/src/canvas.rs +++ /dev/null @@ -1,116 +0,0 @@ -use egui::{ - emath::TSTransform, style::Interaction, Area, Color32, Id, Order, Pos2, Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, Vec2 -}; -use re_viewer_context::InteractionHighlight; - -use crate::ui::draw::DrawableLabel; - -fn register_pan_and_zoom(ui: &Ui, resp: Response, transform: &mut TSTransform) -> Response { - if resp.dragged() { - transform.translation += resp.drag_delta(); - } - - if let Some(mouse_pos) = resp.hover_pos() { - let pointer_in_world = transform.inverse() * mouse_pos; - let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); - let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); - - // Zoom in on pointer, but only if we are not zoomed out too far. - if zoom_delta < 1.0 || transform.scaling < 1.0 { - *transform = *transform - * TSTransform::from_translation(pointer_in_world.to_vec2()) - * TSTransform::from_scaling(zoom_delta) - * TSTransform::from_translation(-pointer_in_world.to_vec2()); - } - - // Pan: - *transform = TSTransform::from_translation(pan_delta) * *transform; - } - - resp -} - -fn fit_to_world_rect(available_size: Vec2, world_rect: Rect) -> TSTransform { - // Compute the scale factor to fit the bounding rectangle into the available screen size. - let scale_x = available_size.x / world_rect.width(); - let scale_y = available_size.y / world_rect.height(); - - // Use the smaller of the two scales to ensure the whole rectangle fits on the screen. - let scale = scale_x.min(scale_y).min(1.0); - - // Compute the translation to center the bounding rect in the screen. - let center_screen = Pos2::new(available_size.x / 2.0, available_size.y / 2.0); - let center_world = world_rect.center().to_vec2(); - - // Set the transformation to scale and then translate to center. - - TSTransform::from_translation(center_screen.to_vec2() - center_world * scale) - * TSTransform::from_scaling(scale) -} - -pub fn draw_node( - ui: &mut Ui, - center: Pos2, - world_to_view: &mut TSTransform, - node: &DrawableLabel, - highlight: InteractionHighlight, -) -> Response { - let resp = { - let builder = UiBuilder::new().max_rect(Rect::from_center_size(center, node.size())); - let mut node_ui = ui.new_child(builder); - node.draw(&mut node_ui, highlight) - }; - register_pan_and_zoom(ui, resp, world_to_view) -} - -pub fn draw_debug(ui: &mut Ui, world_bounding_rect: Rect) { - let painter = ui.painter(); - - // Paint coordinate system at the world origin - let origin = Pos2::new(0.0, 0.0); - let x_axis = Pos2::new(100.0, 0.0); - let y_axis = Pos2::new(0.0, 100.0); - - painter.line_segment([origin, x_axis], Stroke::new(1.0, Color32::RED)); - painter.line_segment([origin, y_axis], Stroke::new(1.0, Color32::GREEN)); - - if world_bounding_rect.is_positive() { - painter.rect( - world_bounding_rect, - 0.0, - Color32::from_rgba_unmultiplied(255, 0, 255, 8), - Stroke::new(1.0, Color32::from_rgb(255, 0, 255)), - ); - } -} - -pub fn zoom_pan_area( - ui: &mut Ui, - view_rect: Rect, - world_bounds: Rect, - id: Id, - draw_contens: impl FnOnce(&mut Ui, &mut TSTransform), -) -> (Response, Rect) { - let mut world_to_view = fit_to_world_rect(view_rect.size(), world_bounds); - let clip_rect_world = world_to_view.inverse() * view_rect; - - let area_resp = Area::new(id.with("view")) - .constrain_to(view_rect) - .order(Order::Middle) - .kind(UiKind::GenericArea) - .show(ui.ctx(), |ui| { - ui.set_clip_rect(clip_rect_world); - - draw_contens(ui, &mut world_to_view); - }); - - // TODO(grtlr): Do we even need an `Area`, or could we just spawn a new `child_ui`? - let resp = ui.allocate_rect(view_rect, Sense::drag()); - let resp = register_pan_and_zoom(ui, resp, &mut world_to_view); - - ui.ctx() - .set_transform_layer(area_resp.response.layer_id, world_to_view); - - let view_size = Rect::from_min_size(Pos2::ZERO, view_rect.size()); - (resp, world_to_view.inverse() * view_size) -} diff --git a/crates/viewer/re_space_view_graph/src/draw.rs b/crates/viewer/re_space_view_graph/src/draw.rs new file mode 100644 index 000000000000..cb6ae4c58d9b --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/draw.rs @@ -0,0 +1,83 @@ +use egui::{Color32, Painter, Pos2, Rect, Response, Sense, Shape, Stroke, Ui, UiBuilder, Vec2}; +use re_viewer_context::InteractionHighlight; + +use crate::ui::draw::DrawableLabel; + +// Sorry for the pun, could not resist 😎. +// On a serious note, is there no other way to create a `Sense` that does nothing? +const NON_SENSE: Sense = Sense { + click: false, + drag: false, + focusable: false, +}; + +/// Draws a node at the given position. +pub fn draw_node( + ui: &mut Ui, + center: Pos2, + node: &DrawableLabel, + highlight: InteractionHighlight, +) -> Response { + let builder = UiBuilder::new().max_rect(Rect::from_center_size(center, node.size())); + let mut node_ui = ui.new_child(builder); + node.draw(&mut node_ui, highlight) +} + +/// Draws a bounding box, as well as a basic coordinate system. +pub fn draw_debug(ui: &Ui, world_bounding_rect: Rect) { + let painter = ui.painter(); + + // Paint coordinate system at the world origin + let origin = Pos2::new(0.0, 0.0); + let x_axis = Pos2::new(100.0, 0.0); + let y_axis = Pos2::new(0.0, 100.0); + + painter.line_segment([origin, x_axis], Stroke::new(1.0, Color32::RED)); + painter.line_segment([origin, y_axis], Stroke::new(1.0, Color32::GREEN)); + + if world_bounding_rect.is_positive() { + painter.rect( + world_bounding_rect, + 0.0, + Color32::from_rgba_unmultiplied(255, 0, 255, 8), + Stroke::new(1.0, Color32::from_rgb(255, 0, 255)), + ); + } +} + +/// Helper function to draw an arrow at the end of the edge +fn draw_arrow(painter: &Painter, tip: Pos2, direction: Vec2, color: Color32) { + let arrow_size = 10.0; // Adjust size as needed + let perpendicular = Vec2::new(-direction.y, direction.x) * 0.5 * arrow_size; + + let p1 = tip - direction * arrow_size + perpendicular; + let p2 = tip - direction * arrow_size - perpendicular; + + // Draw a filled triangle for the arrow + painter.add(Shape::convex_polygon( + vec![tip, p1, p2], + color, + Stroke::NONE, + )); +} + +/// Draws an edge between two points, optionally with an arrow at the target point. +pub fn draw_edge(ui: &mut Ui, points: [Pos2; 2], show_arrow: bool) -> Response { + let fg = ui.style().visuals.text_color(); + + let rect = Rect::from_points(&points); + let painter = ui.painter(); + painter.line_segment(points, Stroke::new(1.0, fg)); + + // Calculate direction vector from source to target + let direction = (points[1] - points[0]).normalized(); + + // Conditionally draw an arrow at the target point + if show_arrow { + draw_arrow(painter, points[1], direction, fg); + } + + // We can add interactions in the future, for now we simply allocate the + // rect, so that bounding boxes are computed correctly. + ui.allocate_rect(rect, NON_SENSE) +} diff --git a/crates/viewer/re_space_view_graph/src/graph/mod.rs b/crates/viewer/re_space_view_graph/src/graph/mod.rs index afb77699156d..5b5e36f3ff15 100644 --- a/crates/viewer/re_space_view_graph/src/graph/mod.rs +++ b/crates/viewer/re_space_view_graph/src/graph/mod.rs @@ -60,7 +60,7 @@ impl Node { pub struct Edge { pub from: NodeIndex, pub to: NodeIndex, - marker: bool, + pub arrow: bool, } pub struct Graph { @@ -119,7 +119,7 @@ impl Graph { let es = data.edges.iter().map(|e| Edge { from: e.source_index, to: e.target_index, - marker: data.graph_type == GraphType::Directed, + arrow: data.graph_type == GraphType::Directed, }); (es.collect(), data.graph_type) diff --git a/crates/viewer/re_space_view_graph/src/layout/layout.rs b/crates/viewer/re_space_view_graph/src/layout/layout.rs index 5972dffdba57..71073078906c 100644 --- a/crates/viewer/re_space_view_graph/src/layout/layout.rs +++ b/crates/viewer/re_space_view_graph/src/layout/layout.rs @@ -11,33 +11,27 @@ pub struct Layout { } impl Layout { + #[deprecated] pub fn bounding_rect(&self) -> Rect { + // TODO(grtlr): We mostly use this for debugging, but we should probably + // take all elements of the layout into account. bounding_rect_from_iter(self.nodes.values().copied()) } /// Gets the final position and size of a node in the layout. - pub fn get_node(&self, node: &NodeIndex) -> Option { - self.nodes.get(node).copied() + /// + /// Returns `Rect::ZERO` if the node is not present in the layout. + pub fn get_node(&self, node: &NodeIndex) -> Rect { + self.nodes.get(node).copied().unwrap_or(Rect::ZERO) } /// Gets the shape of an edge in the final layout. - pub fn get_edge(&self, from: NodeIndex, to: NodeIndex) -> Option { - self.edges.get(&(from, to)).copied() - } - - /// Updates the size and position of a node, for example after size changes. - /// Returns `true` if the node changed its size. - #[deprecated(note = "We should not need to update sizes anymore.")] - pub fn update(&mut self, node: &NodeIndex, rect: Rect) -> bool { - debug_assert!( - self.nodes.contains_key(node), - "node should exist in the layout" - ); - if let Some(extent) = self.nodes.get_mut(node) { - let size_changed = (extent.size() - rect.size()).length_sq() > 0.01; - *extent = rect; - return size_changed; - } - false + /// + /// Returns `[Pos2::ZERO, Pos2::ZERO]` if the edge is not present in the layout. + pub fn get_edge(&self, from: NodeIndex, to: NodeIndex) -> LineSegment { + self.edges + .get(&(from, to)) + .copied() + .unwrap_or([Pos2::ZERO, Pos2::ZERO]) } } diff --git a/crates/viewer/re_space_view_graph/src/layout/provider.rs b/crates/viewer/re_space_view_graph/src/layout/provider.rs index c15042195379..32bc5df23869 100644 --- a/crates/viewer/re_space_view_graph/src/layout/provider.rs +++ b/crates/viewer/re_space_view_graph/src/layout/provider.rs @@ -1,17 +1,14 @@ use egui::{Pos2, Rect, Vec2}; use fjadra as fj; -use crate::graph::{Graph, Node, NodeIndex}; +use crate::graph::NodeIndex; -use super::Layout; +use super::{request::NodeTemplate, Layout, LayoutRequest}; -impl<'a> From<&'a Node> for fj::Node { - fn from(node: &'a Node) -> Self { - match node { - Node::Explicit { - position: Some(pos), - .. - } => Self::default().fixed_position(pos.x as f64, pos.y as f64), +impl<'a> From<&'a NodeTemplate> for fj::Node { + fn from(node: &'a NodeTemplate) -> Self { + match node.fixed_position { + Some(pos) => Self::default().fixed_position(pos.x as f64, pos.y as f64), _ => Self::default(), } } @@ -20,28 +17,35 @@ impl<'a> From<&'a Node> for fj::Node { pub struct ForceLayoutProvider { simulation: fj::Simulation, node_index: ahash::HashMap, + edges: Vec<(NodeIndex, NodeIndex)>, } impl ForceLayoutProvider { - pub fn new(graphs: &[Graph]) -> Self { - let nodes = graphs - .iter() - .flat_map(|g| g.nodes().iter().map(|n| (n.id(), fj::Node::from(n)))); + pub fn new(request: &LayoutRequest) -> Self { + let nodes = request.graphs.iter().flat_map(|(_, graph_template)| { + graph_template + .nodes + .iter() + .map(|n| (n.0, fj::Node::from(n.1))) + }); let mut node_index = ahash::HashMap::default(); let all_nodes: Vec = nodes .enumerate() .map(|(i, n)| { - node_index.insert(n.0, i); + node_index.insert(*n.0, i); n.1 }) .collect(); - let all_edges = graphs.iter().flat_map(|g| { - g.edges() - .iter() - .map(|e| (node_index[&e.from], node_index[&e.to])) - }); + let all_edges_iter = request + .graphs + .iter() + .flat_map(|(_, graph_template)| graph_template.edges.iter()); + + let all_edges = all_edges_iter + .clone() + .map(|(a, b)| (node_index[&a], node_index[&b])); // TODO(grtlr): Currently we guesstimate good forces. Eventually these should be exposed as blueprints. let simulation = fj::SimulationBuilder::default() @@ -59,22 +63,28 @@ impl ForceLayoutProvider { Self { simulation, node_index, + edges: all_edges_iter.cloned().collect(), } } - pub fn init(&self) -> Layout { + pub fn init(&self, request: &LayoutRequest) -> Layout { let positions = self.simulation.positions().collect::>(); let mut extents = ahash::HashMap::default(); - for (node, i) in &self.node_index { - let [x, y] = positions[*i]; - let pos = Pos2::new(x as f32, y as f32); - let size = Vec2::ZERO; - let rect = Rect::from_min_size(pos, size); - extents.insert(*node, rect); + for graph in request.graphs.values() { + for (id, node) in &graph.nodes { + let i = self.node_index[&id]; + let [x, y] = positions[i]; + let pos = Pos2::new(x as f32, y as f32); + extents.insert(*id, Rect::from_center_size(pos, node.size)); + } } - Layout { nodes: extents, edges: ahash::HashMap::default() } + Layout { + nodes: extents, + // Without any real node positions, we probably don't want to draw edges either. + edges: ahash::HashMap::default(), + } } /// Returns `true` if finished. @@ -90,6 +100,50 @@ impl ForceLayoutProvider { extent.set_center(pos); } + for (from, to) in &self.edges { + layout.edges.insert( + (*from, *to), + line_segment(layout.nodes[from], layout.nodes[to]), + ); + } + self.simulation.finished() } } + +/// Helper function to calculate the line segment between two rectangles. +fn line_segment(source: Rect, target: Rect) -> [Pos2; 2] { + let source_center = source.center(); + let target_center = target.center(); + + // Calculate direction vector from source to target + let direction = (target_center - source_center).normalized(); + + // Find the border points on both rectangles + let source_point = find_border_point(source, -direction); // Reverse direction for target + let target_point = find_border_point(target, direction); + + [source_point, target_point] +} + +/// Helper function to find the point where the line intersects the border of a rectangle +fn find_border_point(rect: Rect, direction: Vec2) -> Pos2 { + let mut t_min = f32::NEG_INFINITY; + let mut t_max = f32::INFINITY; + + for i in 0..2 { + let inv_d = 1.0 / direction[i]; + let mut t0 = (rect.min[i] - rect.center()[i]) * inv_d; + let mut t1 = (rect.max[i] - rect.center()[i]) * inv_d; + + if inv_d < 0.0 { + std::mem::swap(&mut t0, &mut t1); + } + + t_min = t_min.max(t0); + t_max = t_max.min(t1); + } + + let t = t_max.min(t_min); // Pick the first intersection + rect.center() + t * direction +} diff --git a/crates/viewer/re_space_view_graph/src/layout/request.rs b/crates/viewer/re_space_view_graph/src/layout/request.rs index a59ee5d83306..cc25d78bc5de 100644 --- a/crates/viewer/re_space_view_graph/src/layout/request.rs +++ b/crates/viewer/re_space_view_graph/src/layout/request.rs @@ -6,20 +6,20 @@ use re_chunk::EntityPath; use crate::graph::{Graph, NodeIndex}; #[derive(PartialEq)] -struct NodeTemplate { - size: Vec2, - fixed_position: Option, +pub(super) struct NodeTemplate { + pub(super) size: Vec2, + pub(super) fixed_position: Option, } #[derive(Default, PartialEq)] -struct GraphTemplate { - nodes: BTreeMap, - edges: BTreeSet<(NodeIndex, NodeIndex)>, +pub(super) struct GraphTemplate { + pub(super) nodes: BTreeMap, + pub(super) edges: BTreeSet<(NodeIndex, NodeIndex)>, } #[derive(PartialEq)] pub struct LayoutRequest { - graphs: BTreeMap, + pub(super) graphs: BTreeMap, } impl LayoutRequest { diff --git a/crates/viewer/re_space_view_graph/src/lib.rs b/crates/viewer/re_space_view_graph/src/lib.rs index 4022d8ae9698..a3f95a9c2601 100644 --- a/crates/viewer/re_space_view_graph/src/lib.rs +++ b/crates/viewer/re_space_view_graph/src/lib.rs @@ -2,12 +2,12 @@ //! //! A Space View that shows a graph (node-link diagram). +mod draw; mod graph; mod layout; mod properties; mod ui; mod view; mod visualizers; -mod canvas; pub use view::GraphSpaceView; diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/edge.rs b/crates/viewer/re_space_view_graph/src/ui/draw/edge.rs deleted file mode 100644 index 501614fe7655..000000000000 --- a/crates/viewer/re_space_view_graph/src/ui/draw/edge.rs +++ /dev/null @@ -1,67 +0,0 @@ -use egui::{Color32, Frame, Painter, Pos2, Rect, Response, Shape, Stroke, Ui, Vec2}; - -/// Draws an edge with an optional arrow mark at the target end. -pub fn draw_edge(ui: &mut Ui, source: Rect, target: Rect, show_arrow: bool) -> Response { - let fg = ui.style().visuals.text_color(); - - Frame::default() - .show(ui, |ui| { - let source_center = source.center(); - let target_center = target.center(); - - // Calculate direction vector from source to target - let direction = (target_center - source_center).normalized(); - - // Find the border points on both rectangles - let source_point = find_border_point(source, -direction); // Reverse direction for target - let target_point = find_border_point(target, direction); - - let painter = ui.painter(); - - painter.line_segment([source_point, target_point], Stroke::new(1.0, fg)); - - // Conditionally draw an arrow at the target point - if show_arrow { - draw_arrow(painter, target_point, direction, fg); - } - }) - .response -} - -/// Helper function to find the point where the line intersects the border of a rectangle -fn find_border_point(rect: Rect, direction: Vec2) -> Pos2 { - let mut t_min = f32::NEG_INFINITY; - let mut t_max = f32::INFINITY; - - for i in 0..2 { - let inv_d = 1.0 / direction[i]; - let mut t0 = (rect.min[i] - rect.center()[i]) * inv_d; - let mut t1 = (rect.max[i] - rect.center()[i]) * inv_d; - - if inv_d < 0.0 { - std::mem::swap(&mut t0, &mut t1); - } - - t_min = t_min.max(t0); - t_max = t_max.min(t1); - } - - let t = t_max.min(t_min); // Pick the first intersection - rect.center() + t * direction -} - -/// Helper function to draw an arrow at the end of the edge -fn draw_arrow(painter: &Painter, tip: Pos2, direction: Vec2, color: Color32) { - let arrow_size = 10.0; // Adjust size as needed - let perpendicular = Vec2::new(-direction.y, direction.x) * 0.5 * arrow_size; - - let p1 = tip - direction * arrow_size + perpendicular; - let p2 = tip - direction * arrow_size - perpendicular; - - // Draw a filled triangle for the arrow - painter.add(Shape::convex_polygon( - vec![tip, p1, p2], - color, - Stroke::NONE, - )); -} diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs b/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs index 7520b81a3cb1..d9d5243df73c 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs @@ -1,7 +1,5 @@ -mod edge; mod entity; mod node; -pub use edge::draw_edge; pub use entity::draw_entity; -pub use node::{DrawableLabel}; +pub use node::DrawableLabel; diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/node.rs b/crates/viewer/re_space_view_graph/src/ui/draw/node.rs index e1579e09f106..0bf4b00fbfd5 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw/node.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw/node.rs @@ -94,7 +94,11 @@ impl DrawableLabel { Self::Text(TextLabel { galley, frame }) => { frame .show(ui, |ui| { - ui.add(egui::Label::new(galley.clone()).selectable(false).sense(sense)) + ui.add( + egui::Label::new(galley.clone()) + .selectable(false) + .sense(sense), + ) }) .response } diff --git a/crates/viewer/re_space_view_graph/src/ui/mod.rs b/crates/viewer/re_space_view_graph/src/ui/mod.rs index dfd875cc82b4..ad63d4588bd5 100644 --- a/crates/viewer/re_space_view_graph/src/ui/mod.rs +++ b/crates/viewer/re_space_view_graph/src/ui/mod.rs @@ -3,7 +3,7 @@ mod state; pub mod scene; -pub use state::{Discriminator, GraphSpaceViewState}; +pub use state::GraphSpaceViewState; pub fn bounding_rect_from_iter(rectangles: impl Iterator) -> egui::Rect { rectangles.fold(egui::Rect::NOTHING, |acc, rect| acc.union(rect)) diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index cb956eb8f461..ea428bdff6a3 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -4,10 +4,7 @@ use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; use re_viewer_context::SpaceViewState; -use crate::{ - graph::Graph, - layout::{ForceLayoutProvider, Layout}, -}; +use crate::layout::{ForceLayoutProvider, Layout, LayoutRequest}; /// Space view state for the custom space view. /// @@ -60,8 +57,6 @@ impl SpaceViewState for GraphSpaceViewState { } } -pub type Discriminator = u64; - /// The following is a simple state machine that keeps track of the different /// layouts and if they need to be recomputed. It also holds the state of the /// force-based simulation. @@ -70,12 +65,12 @@ pub enum LayoutState { #[default] None, InProgress { - discriminator: Discriminator, + request: LayoutRequest, layout: Layout, provider: ForceLayoutProvider, }, Finished { - discriminator: Discriminator, + request: LayoutRequest, layout: Layout, _provider: ForceLayoutProvider, }, @@ -104,54 +99,46 @@ impl LayoutState { } /// A simple state machine that keeps track of the different stages and if the layout needs to be recomputed. - fn update( - self, - requested: Discriminator, - graphs: &[Graph], - ) -> Self { + fn update(self, new_request: LayoutRequest) -> Self { match self { // Layout is up to date, nothing to do here. - Self::Finished { - ref discriminator, .. - } if discriminator == &requested => { + Self::Finished { ref request, .. } if request == &new_request => { self // no op } // We need to recompute the layout. Self::None | Self::Finished { .. } => { - let provider = ForceLayoutProvider::new(graphs); - let layout = provider.init(); + let provider = ForceLayoutProvider::new(&new_request); + let layout = provider.init(&new_request); Self::InProgress { - discriminator: requested, + request: new_request, layout, provider, } } - Self::InProgress { - ref discriminator, .. - } if discriminator != &requested => { - let provider = ForceLayoutProvider::new(graphs); - let layout = provider.init(); + Self::InProgress { request, .. } if request != new_request => { + let provider = ForceLayoutProvider::new(&new_request); + let layout = provider.init(&new_request); Self::InProgress { - discriminator: requested, + request: new_request, layout, provider, } } // We keep iterating on the layout until it is stable. Self::InProgress { - discriminator, + request, mut layout, mut provider, } => match provider.tick(&mut layout) { true => Self::Finished { - discriminator, + request, layout, _provider: provider, }, false => Self::InProgress { - discriminator, + request, layout, provider, }, @@ -160,12 +147,8 @@ impl LayoutState { } /// This method is lazy. A new layout is only computed if the current timestamp requires it. - pub fn get<'a>( - &'a mut self, - hash: Discriminator, - graphs: &[Graph], - ) -> &'a mut Layout { - *self = std::mem::take(self).update(hash, graphs); + pub fn get(&mut self, request: LayoutRequest) -> &mut Layout { + *self = std::mem::take(self).update(request); match self { Self::Finished { layout, .. } | Self::InProgress { layout, .. } => layout, diff --git a/crates/viewer/re_space_view_graph/src/view.rs b/crates/viewer/re_space_view_graph/src/view.rs index eb47a405ca2c..1cc312d94384 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -1,4 +1,3 @@ -use egui::{emath::TSTransform, Color32, Layout, Pos2, Stroke}; use re_log_types::EntityPath; use re_space_view::{ controls::{DRAG_PAN2D_BUTTON, ZOOM_SCROLL_MODIFIER}, @@ -6,21 +5,25 @@ use re_space_view::{ }; use re_types::{ blueprint::{self, archetypes::VisualBounds2D}, - components, SpaceViewClassIdentifier, + SpaceViewClassIdentifier, +}; +use re_ui::{ + self, zoom_pan_area::zoom_pan_area, ModifiersMarkdown, MouseButtonMarkdown, UiExt as _, }; -use re_ui::{self, ModifiersMarkdown, MouseButtonMarkdown, UiExt as _}; use re_viewer_context::{ - external::re_entity_db::InstancePath, IdentifiedViewSystem as _, Item, RecommendedSpaceView, - SpaceViewClass, SpaceViewClassLayoutPriority, SpaceViewClassRegistryError, SpaceViewId, - SpaceViewSpawnHeuristics, SpaceViewState, SpaceViewStateExt as _, - SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, SystemExecutionOutput, ViewQuery, - ViewerContext, + IdentifiedViewSystem as _, RecommendedSpaceView, SpaceViewClass, SpaceViewClassLayoutPriority, + SpaceViewClassRegistryError, SpaceViewId, SpaceViewSpawnHeuristics, SpaceViewState, + SpaceViewStateExt as _, SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, + SystemExecutionOutput, ViewQuery, ViewerContext, }; use re_viewport_blueprint::ViewProperty; -use std::hash::{Hash as _, Hasher as _}; use crate::{ - canvas::{draw_debug, draw_node, zoom_pan_area}, graph::Graph, layout::LayoutRequest, ui::{draw::DrawableLabel, Discriminator, GraphSpaceViewState}, visualizers::{merge, EdgesVisualizer, NodeVisualizer} + draw::{draw_debug, draw_edge, draw_node}, + graph::Graph, + layout::LayoutRequest, + ui::GraphSpaceViewState, + visualizers::{merge, EdgesVisualizer, NodeVisualizer}, }; #[derive(Default)] @@ -161,39 +164,29 @@ Display a graph of nodes and edges. let view_rect = ui.max_rect(); let request = LayoutRequest::from_graphs(graphs.iter()); - - // The descriminator is used to determine if the layout needs to be recomputed. - let discriminator = { - let mut hasher = ahash::AHasher::default(); - for graph in &graphs { - graph.size_hash().hash(&mut hasher); - } - hasher.finish() - }; - - let layout = state.layout_state.get(discriminator, &graphs); + let layout = state.layout_state.get(request); let (resp, new_bounds) = zoom_pan_area( ui, view_rect, bounds.into(), egui::Id::new(query.space_view_id), - |ui, world_to_view| { + |ui| { let mut world_bounding_rect = egui::Rect::NOTHING; for graph in graphs { for node in graph.nodes() { - // TODO(grtlr): provide debug assertions here. - let center = layout.get_node(&node.id()).unwrap_or(egui::Rect::ZERO).center(); + let center = layout.get_node(&node.id()).center(); // TODO(grtlr): Add proper highlights here: - let resp = draw_node( - ui, - center, - world_to_view, - node.label(), - Default::default(), - ); + let resp = + draw_node(ui, center, node.label(), Default::default()); + world_bounding_rect = world_bounding_rect.union(resp.rect); + } + + for edge in graph.edges() { + let points = layout.get_edge(edge.from, edge.to); + let resp = draw_edge(ui, points, edge.arrow); world_bounding_rect = world_bounding_rect.union(resp.rect); } } diff --git a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs index ceb658223551..840de2853d4e 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs @@ -4,7 +4,7 @@ mod nodes; use std::collections::BTreeSet; pub use edges::{EdgeData, EdgeInstance, EdgesVisualizer}; -pub use nodes::{NodeData, NodeInstance, NodeVisualizer, Label}; +pub use nodes::{Label, NodeData, NodeInstance, NodeVisualizer}; use re_chunk::EntityPath; diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index efb27a0a1c59..7c5535319205 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -7,6 +7,7 @@ mod syntax_highlighting; mod context_ext; pub mod drag_and_drop; +pub mod zoom_pan_area; pub mod icons; pub mod list_item; mod markdown_utils; diff --git a/crates/viewer/re_ui/src/zoom_pan_area.rs b/crates/viewer/re_ui/src/zoom_pan_area.rs new file mode 100644 index 000000000000..c7850c70ade2 --- /dev/null +++ b/crates/viewer/re_ui/src/zoom_pan_area.rs @@ -0,0 +1,97 @@ +use egui::{emath::TSTransform, Area, Id, Order, Pos2, 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_world: &mut TSTransform) { + if resp.dragged() { + ui_from_world.translation += ui_from_world.scaling * resp.drag_delta(); + } + + if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) { + if resp.contains_pointer() { + let pointer_in_world = ui_from_world.inverse() * mouse_pos; + let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); + let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); + + // Zoom in on pointer, but only if we are not zoomed out too far. + if zoom_delta < 1.0 || ui_from_world.scaling < 1.0 { + *ui_from_world = *ui_from_world + * TSTransform::from_translation(pointer_in_world.to_vec2()) + * TSTransform::from_scaling(zoom_delta) + * TSTransform::from_translation(-pointer_in_world.to_vec2()); + } + + // Pan: + *ui_from_world = TSTransform::from_translation(pan_delta) * *ui_from_world; + } + } +} + +/// Creates a transformation that fits a given world rectangle into the available screen size. +pub fn fit_to_world_rect(available_size: Vec2, world_rect: Rect) -> TSTransform { + // Compute the scale factor to fit the bounding rectangle into the available screen size. + let scale_x = available_size.x / world_rect.width(); + let scale_y = available_size.y / world_rect.height(); + + // Use the smaller of the two scales to ensure the whole rectangle fits on the screen. + let scale = scale_x.min(scale_y).min(1.0); + + // Compute the translation to center the bounding rect in the screen. + let center_screen = Pos2::new(available_size.x / 2.0, available_size.y / 2.0); + let center_world = world_rect.center().to_vec2(); + + // Set the transformation to scale and then translate to center. + + TSTransform::from_translation(center_screen.to_vec2() - center_world * scale) + * TSTransform::from_scaling(scale) +} + +/// Provides a zoom-pan area for a given view. +pub fn zoom_pan_area( + ui: &Ui, + view_bounds_ui: Rect, + world_bounds: Rect, + // TODO(grtlr): Can we get rid of the `Id` here? + id: Id, + draw_contens: impl FnOnce(&mut Ui), +) -> (Response, Rect) { + // ui space = global egui space + // world-space = the space where we put graph nodes + let mut ui_from_world = fit_to_world_rect(view_bounds_ui.size(), world_bounds); + + let area_resp = Area::new(id.with("zoom_pan_area")) + .constrain_to(view_bounds_ui) + .order(Order::Middle) + .kind(UiKind::GenericArea) + .show(ui.ctx(), |ui| { + // Transform to the world space: + let visible_rect_in_world = ui_from_world.inverse() * view_bounds_ui; + + ui.set_clip_rect(visible_rect_in_world); // set proper clip-rect so we can interact with the background. TODO: why is this needed? + + // 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::drag()) + .max_rect(visible_rect_in_world), + ); + drag_sense_ui.set_min_size(visible_rect_in_world.size()); + let pan_response = drag_sense_ui.response(); + + // Update the transform based on the interactions: + register_pan_and_zoom(ui, &pan_response, &mut ui_from_world); + + // Update the clip-rect with the new transform, to avoid frame-delays + ui.set_clip_rect(ui_from_world.inverse() * view_bounds_ui); + + // Add the actul contents to the area: + draw_contens(ui); + + pan_response + }); + + ui.ctx() + .set_transform_layer(area_resp.response.layer_id, ui_from_world); + + let view_size = Rect::from_min_size(Pos2::ZERO, view_bounds_ui.size()); + (area_resp.inner, ui_from_world.inverse() * view_size) +}