From a16219f067b7a3b38c64262018d8b4c18e7be270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Thu, 10 Oct 2024 10:25:32 +0200 Subject: [PATCH] WIP: layout switching and convenience --- Cargo.lock | 7 + examples/rust/graph_view/Cargo.toml | 5 +- examples/rust/graph_view/src/layout/dot.rs | 122 ++++++++++++++++++ .../graph_view/src/layout/force_directed.rs | 17 +-- examples/rust/graph_view/src/layout/mod.rs | 38 +++++- examples/rust/graph_view/src/ui/mod.rs | 69 +--------- examples/rust/graph_view/src/ui/state.rs | 118 +++++++++++++++++ examples/rust/graph_view/src/view.rs | 6 +- 8 files changed, 296 insertions(+), 86 deletions(-) create mode 100644 examples/rust/graph_view/src/layout/dot.rs create mode 100644 examples/rust/graph_view/src/ui/state.rs diff --git a/Cargo.lock b/Cargo.lock index ed5661493cbc..c8b1da356f94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2697,6 +2697,7 @@ version = "0.0.0" dependencies = [ "bytemuck", "fdg-sim", + "layout-rs", "mimalloc", "petgraph", "re_crash_handler", @@ -3173,6 +3174,12 @@ dependencies = [ "libc", ] +[[package]] +name = "layout-rs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84deb28a3a6c839ca42a7341664f32281416d69e2f29deb85aec5cc0243fdea8" + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/examples/rust/graph_view/Cargo.toml b/examples/rust/graph_view/Cargo.toml index c0e79bdfe4e0..fb1344cd7765 100644 --- a/examples/rust/graph_view/Cargo.toml +++ b/examples/rust/graph_view/Cargo.toml @@ -27,5 +27,8 @@ mimalloc = "0.1" petgraph = "0.6" bytemuck = "1.18" -fdg-sim = "0.9" thiserror = "1.0" + +# Experiment with different layout algorithms. +fdg-sim = "0.9" +layout-rs = "0.1" diff --git a/examples/rust/graph_view/src/layout/dot.rs b/examples/rust/graph_view/src/layout/dot.rs new file mode 100644 index 000000000000..c284997d06e5 --- /dev/null +++ b/examples/rust/graph_view/src/layout/dot.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; + +use layout::{ + core::{ + base::Orientation, + format::{ClipHandle, RenderBackend}, + geometry::Point, + style::StyleAttr, + }, + std_shapes::shapes::{Arrow, Element, ShapeKind}, + topo::layout::VisualGraph, +}; +use re_viewer::external::egui; + +use crate::{error::Error, types::NodeIndex}; + +use super::Layout; + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct DotLayout; + +impl Layout for DotLayout { + type NodeIx = NodeIndex; + + fn compute( + &self, + nodes: impl IntoIterator, + directed: impl IntoIterator, + undirected: impl IntoIterator, + ) -> Result, Error> { + let mut handle_to_ix = HashMap::new(); + let mut ix_to_handle = HashMap::new(); + + let mut graph = VisualGraph::new(Orientation::TopToBottom); + + for (ix, size) in nodes { + let size = Point::new(size.x as f64, size.y as f64); + let handle = graph.add_node(Element::create( + ShapeKind::new_box("test"), + StyleAttr::simple(), + Orientation::LeftToRight, + size, + )); + handle_to_ix.insert(handle, ix.clone()); + ix_to_handle.insert(ix, handle); + } + + for (source_ix, target_ix) in directed { + let source = ix_to_handle + .get(&source_ix) + .ok_or_else(|| Error::EdgeUnknownNode(source_ix.to_string()))?; + let target = ix_to_handle + .get(&target_ix) + .ok_or_else(|| Error::EdgeUnknownNode(target_ix.to_string()))?; + graph.add_edge(Arrow::simple("test"), *source, *target); + } + + for (source_ix, target_ix) in undirected { + let source = ix_to_handle + .get(&source_ix) + .ok_or_else(|| Error::EdgeUnknownNode(source_ix.to_string()))?; + let target = ix_to_handle + .get(&target_ix) + .ok_or_else(|| Error::EdgeUnknownNode(target_ix.to_string()))?; + + // TODO(grtlr): find a better way other than adding duplicate edges. + graph.add_edge(Arrow::simple("test"), *source, *target); + graph.add_edge(Arrow::simple("test"), *target, *source); + } + + graph.do_it(false, false, false, &mut DummyBackend); + + let res = handle_to_ix + .into_iter() + .map(|(h, ix)| { + let (min, max) = graph.pos(h).bbox(false); + ( + ix, + egui::Rect::from_min_max( + egui::Pos2::new(min.x as f32, min.y as f32), + egui::Pos2::new(max.x as f32, max.y as f32), + ), + ) + }) + .collect(); + + Ok(res) + } +} + +struct DummyBackend; + +impl RenderBackend for DummyBackend { + fn draw_rect( + &mut self, + _xy: Point, + _size: Point, + _look: &StyleAttr, + _clip: Option, + ) { + } + + fn draw_line(&mut self, _start: Point, _stop: Point, _look: &StyleAttr) {} + + fn draw_circle(&mut self, _xy: Point, _size: Point, _look: &StyleAttr) {} + + fn draw_text(&mut self, _xy: Point, _text: &str, _look: &StyleAttr) {} + + fn draw_arrow( + &mut self, + _path: &[(Point, Point)], + _dashed: bool, + _head: (bool, bool), + _look: &StyleAttr, + _text: &str, + ) { + } + + fn create_clip(&mut self, _xy: Point, _size: Point, _rounded_px: usize) -> ClipHandle { + ClipHandle::default() + } +} diff --git a/examples/rust/graph_view/src/layout/force_directed.rs b/examples/rust/graph_view/src/layout/force_directed.rs index 03ac998a65ee..2d1d68f7525a 100644 --- a/examples/rust/graph_view/src/layout/force_directed.rs +++ b/examples/rust/graph_view/src/layout/force_directed.rs @@ -5,23 +5,14 @@ use re_viewer::external::egui; use crate::{error::Error, types::NodeIndex}; -use super::LayoutProvider; +use super::Layout; +#[derive(Debug, Default, PartialEq, Eq)] pub struct ForceBasedLayout; -impl ForceBasedLayout { - pub fn new() -> Self { - Self - } -} - -impl LayoutProvider for ForceBasedLayout { +impl Layout for ForceBasedLayout { type NodeIx = NodeIndex; - fn name() -> &'static str { - "Force Directed" - } - fn compute( &self, nodes: impl IntoIterator, @@ -36,7 +27,7 @@ impl LayoutProvider for ForceBasedLayout { node_to_index.insert(node_id, ix); } - for (source, target) in directed.into_iter().chain(undirected).into_iter() { + for (source, target) in directed.into_iter().chain(undirected) { let source_ix = node_to_index .get(&source) .ok_or_else(|| Error::EdgeUnknownNode(source.to_string()))?; diff --git a/examples/rust/graph_view/src/layout/mod.rs b/examples/rust/graph_view/src/layout/mod.rs index 4b12f06e9aaa..ba5dbfcf4e3c 100644 --- a/examples/rust/graph_view/src/layout/mod.rs +++ b/examples/rust/graph_view/src/layout/mod.rs @@ -2,16 +2,16 @@ use std::collections::HashMap; use re_viewer::external::egui; -use crate::error::Error; +use crate::{error::Error, types::NodeIndex}; +mod dot; +pub(crate) use dot::DotLayout; mod force_directed; pub(crate) use force_directed::ForceBasedLayout; -pub(crate) trait LayoutProvider { +pub(crate) trait Layout { type NodeIx: Clone + Eq + std::hash::Hash; - fn name() -> &'static str; - fn compute( &self, nodes: impl IntoIterator, @@ -19,3 +19,33 @@ pub(crate) trait LayoutProvider { undirected: impl IntoIterator, ) -> Result, Error>; } + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum LayoutProvider { + Dot(DotLayout), + ForceDirected(ForceBasedLayout), +} + +impl LayoutProvider { + pub(crate) fn new_dot() -> Self { + LayoutProvider::Dot(Default::default()) + } + + pub(crate) fn new_force_directed() -> Self { + LayoutProvider::ForceDirected(Default::default()) + } +} + +impl LayoutProvider { + pub(crate) fn compute( + &self, + nodes: impl IntoIterator, + directed: impl IntoIterator, + undirected: impl IntoIterator, + ) -> Result, Error> { + match self { + LayoutProvider::Dot(layout) => layout.compute(nodes, directed, undirected), + LayoutProvider::ForceDirected(layout) => layout.compute(nodes, directed, undirected), + } + } +} diff --git a/examples/rust/graph_view/src/ui/mod.rs b/examples/rust/graph_view/src/ui/mod.rs index 55dec35ae363..5e0b166886ee 100644 --- a/examples/rust/graph_view/src/ui/mod.rs +++ b/examples/rust/graph_view/src/ui/mod.rs @@ -13,8 +13,10 @@ use re_viewer::external::{ mod edge; pub(crate) use edge::draw_edge; +mod state; +pub(crate) use state::GraphSpaceViewState; -use crate::{graph::Node, types::{NodeIndex, NodeInstance, UnknownNodeInstance}}; +use crate::{graph::Node, layout::LayoutProvider, types::{NodeIndex, NodeInstance, UnknownNodeInstance}}; pub fn draw_node( ui: &mut egui::Ui, @@ -128,20 +130,6 @@ pub fn measure_node_sizes<'a>( sizes } -/// Space view state for the custom space view. -/// -/// This state is preserved between frames, but not across Viewer sessions. -#[derive(Default)] -pub(crate) struct GraphSpaceViewState { - pub world_to_view: emath::TSTransform, - - // Debug information - pub show_debug: bool, - - /// Positions of the nodes in world space. - pub layout: Option>, -} - pub fn bounding_rect_from_iter<'a>( rectangles: impl Iterator, ) -> Option { @@ -157,54 +145,3 @@ pub fn bounding_rect_from_iter<'a>( bounding_rect } - -impl GraphSpaceViewState { - pub fn fit_to_screen(&mut self, bounding_rect: egui::Rect, available_size: egui::Vec2) { - // Compute the scale factor to fit the bounding rectangle into the available screen size. - let scale_x = available_size.x / bounding_rect.width(); - let scale_y = available_size.y / bounding_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 = egui::Pos2::new(available_size.x / 2.0, available_size.y / 2.0); - let center_world = bounding_rect.center().to_vec2(); - - // Set the transformation to scale and then translate to center. - self.world_to_view = - emath::TSTransform::from_translation(center_screen.to_vec2() - center_world * scale) - * emath::TSTransform::from_scaling(scale); - } - - pub fn bounding_box_ui(&mut self, ui: &mut egui::Ui) { - if let Some(layout) = &self.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); - if let Some(egui::Rect { min, max }) = bounding_rect_from_iter(layout.values()) { - ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); - ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); - } - }); - ui.end_row(); - } - } - - pub fn debug_ui(&mut self, ui: &mut egui::Ui) { - ui.re_checkbox(&mut self.show_debug, "Show debug information") - .on_hover_text("Shows debug information for the current graph"); - ui.end_row(); - } -} - -impl SpaceViewState for GraphSpaceViewState { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } -} diff --git a/examples/rust/graph_view/src/ui/state.rs b/examples/rust/graph_view/src/ui/state.rs new file mode 100644 index 000000000000..ab3d67f7f42e --- /dev/null +++ b/examples/rust/graph_view/src/ui/state.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +use re_format::format_f32; +use re_viewer::external::{ + egui::{self, emath}, + re_ui::UiExt, + re_viewer_context::SpaceViewState, +}; + +use crate::{layout::LayoutProvider, types::NodeIndex}; + +use super::bounding_rect_from_iter; + +/// Space view state for the custom space view. +/// +/// This state is preserved between frames, but not across Viewer sessions. +pub(crate) struct GraphSpaceViewState { + pub world_to_view: emath::TSTransform, + pub clip_rect_window: egui::Rect, + + // Debug information + pub show_debug: bool, + + /// Positions of the nodes in world space. + pub layout: Option>, + pub layout_provider: LayoutProvider, +} + +impl Default for GraphSpaceViewState { + fn default() -> Self { + Self { + world_to_view: Default::default(), + clip_rect_window: egui::Rect::NOTHING, + show_debug: Default::default(), + layout: Default::default(), + layout_provider: LayoutProvider::new_dot(), + } + } +} + +impl GraphSpaceViewState { + pub fn fit_to_screen(&mut self, bounding_rect: egui::Rect, available_size: egui::Vec2) { + // Compute the scale factor to fit the bounding rectangle into the available screen size. + let scale_x = available_size.x / bounding_rect.width(); + let scale_y = available_size.y / bounding_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 = egui::Pos2::new(available_size.x / 2.0, available_size.y / 2.0); + let center_world = bounding_rect.center().to_vec2(); + + // Set the transformation to scale and then translate to center. + self.world_to_view = + emath::TSTransform::from_translation(center_screen.to_vec2() - center_world * scale) + * emath::TSTransform::from_scaling(scale); + } + + pub fn bounding_box_ui(&mut self, ui: &mut egui::Ui) { + if let Some(layout) = &self.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); + if let Some(egui::Rect { min, max }) = bounding_rect_from_iter(layout.values()) { + ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); + ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); + } + }); + ui.end_row(); + if ui + .button("Fit to screen") + .on_hover_text("Fit the bounding box to the screen") + .clicked() + { + if let Some(bounding_rect) = bounding_rect_from_iter(layout.values()) { + self.fit_to_screen(bounding_rect, self.clip_rect_window.size()); + } + } + ui.end_row(); + } + } + + pub fn debug_ui(&mut self, ui: &mut egui::Ui) { + ui.re_checkbox(&mut self.show_debug, "Show debug information") + .on_hover_text("Shows debug information for the current graph"); + ui.end_row(); + } + + pub fn layout_provider_ui(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.label("Layout algorithm:"); + + let layout_options = [ + (LayoutProvider::new_dot(), "Dot"), + (LayoutProvider::new_force_directed(), "Force Directed"), + ]; + + for (l, t) in layout_options { + if ui.re_radio_value(&mut self.layout_provider, l, t).changed() { + self.layout = None + }; + } + }); + ui.end_row(); + } +} + +impl SpaceViewState for GraphSpaceViewState { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} diff --git a/examples/rust/graph_view/src/view.rs b/examples/rust/graph_view/src/view.rs index aa54bb52491d..1e07cd93d86b 100644 --- a/examples/rust/graph_view/src/view.rs +++ b/examples/rust/graph_view/src/view.rs @@ -16,7 +16,6 @@ use re_viewer::external::{ use crate::{ error::Error, graph::Graph, - layout::{ForceBasedLayout, LayoutProvider}, types::NodeIndex, ui::{self, GraphSpaceViewState}, visualizers::{EdgesDirectedVisualizer, EdgesUndirectedVisualizer, NodeVisualizer}, @@ -96,6 +95,7 @@ impl SpaceViewClass for GraphSpaceView { ui.selection_grid("graph_settings_ui").show(ui, |ui| { state.bounding_box_ui(ui); state.debug_ui(ui); + state.layout_provider_ui(ui); }); Ok(()) @@ -123,7 +123,9 @@ impl SpaceViewClass for GraphSpaceView { let graph = Graph::from_nodes_edges(&node_system.data, &undirected_system.data); let state = state.downcast_mut::()?; + let (id, clip_rect_window) = ui.allocate_space(ui.available_size()); + state.clip_rect_window = clip_rect_window; let Some(layout) = &mut state.layout else { let node_sizes = ui::measure_node_sizes(ui, graph.all_nodes()); @@ -138,7 +140,7 @@ impl SpaceViewClass for GraphSpaceView { .iter() .flat_map(|d| d.edges().map(|e| (e.source.into(), e.target.into()))); - let layout = ForceBasedLayout::new().compute(node_sizes.into_iter(), undirected, directed)?; + let layout = state.layout_provider.compute(node_sizes.into_iter(), undirected, directed)?; if let Some(bounding_box) = ui::bounding_rect_from_iter(layout.values()) { state.fit_to_screen(