Skip to content

Commit

Permalink
WIP: basic blueprint support
Browse files Browse the repository at this point in the history
  • Loading branch information
grtlr committed Oct 28, 2024
1 parent 0674ef0 commit 81d556d
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 90 deletions.
2 changes: 1 addition & 1 deletion crates/viewer/re_space_view_graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
mod error;
mod graph;
mod properties;
mod types;
mod ui;
mod view;
mod properties;
mod visualizers;

pub use view::GraphSpaceView;
5 changes: 4 additions & 1 deletion crates/viewer/re_space_view_graph/src/properties.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use re_types::blueprint::components::VisualBounds2D;
use re_viewer_context::{SpaceViewStateExt as _, TypedComponentFallbackProvider};

use crate::{ui::{bounding_rect_from_iter, GraphSpaceViewState}, GraphSpaceView};
use crate::{
ui::{bounding_rect_from_iter, GraphSpaceViewState},
GraphSpaceView,
};

fn valid_bound(rect: &egui::Rect) -> bool {
rect.is_finite() && rect.is_positive()
Expand Down
4 changes: 1 addition & 3 deletions crates/viewer/re_space_view_graph/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ pub fn draw_entity(
response
}

pub fn bounding_rect_from_iter<'a>(
rectangles: impl Iterator<Item = &'a egui::Rect>,
) -> egui::Rect {
pub fn bounding_rect_from_iter<'a>(rectangles: impl Iterator<Item = &'a egui::Rect>) -> egui::Rect {
rectangles.fold(egui::Rect::NOTHING, |acc, rect| acc.union(*rect))
}
92 changes: 39 additions & 53 deletions crates/viewer/re_space_view_graph/src/ui/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,63 @@ use egui::{
emath::TSTransform, Area, Color32, Id, LayerId, Order, Painter, Pos2, Rect, Response, Sense,
Stroke, Ui,
};
use re_log::external::log;

fn fit_to_world_rect(clip_rect_window: Rect, world_rect: Rect) -> TSTransform {
let available_size = clip_rect_window.size();

// 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 struct ViewBuilder {
world_to_view: TSTransform,
clip_rect_window: Rect,
// TODO(grtlr): separate state from builder
pub show_debug: bool,
show_debug: bool,
world_bounds: Rect,
bounding_rect: Rect,
}

impl Default for ViewBuilder {
fn default() -> Self {
impl ViewBuilder {
pub fn from_world_bounds(world_bounds: impl Into<Rect>) -> Self {
Self {
world_to_view: Default::default(),
clip_rect_window: Rect::NOTHING,
world_bounds: world_bounds.into(),
show_debug: false,
bounding_rect: Rect::NOTHING,
}
}
}

impl ViewBuilder {
fn fit_to_rect(&mut self, bounding_rect: Rect) {
let available_size = self.clip_rect_window.size();

// 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 = 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 =
TSTransform::from_translation(center_screen.to_vec2() - center_world * scale)
* TSTransform::from_scaling(scale);
}

pub fn fit_to_screen(&mut self) {
self.fit_to_rect(self.bounding_rect);
pub fn show_debug(&mut self) {
self.show_debug = true;
}

/// Return the clip rect of the scene in window coordinates.
pub fn scene<F>(&mut self, ui: &mut Ui, add_scene_contents: F) -> Rect
pub fn scene<F>(mut self, ui: &mut Ui, add_scene_contents: F) -> (Rect, Response)
where
F: for<'b> FnOnce(Scene<'b>),
{
let (id, clip_rect_window) = ui.allocate_space(ui.available_size());
self.clip_rect_window = clip_rect_window;

let response = ui.interact(clip_rect_window, id, Sense::click_and_drag());

let mut world_to_view = fit_to_world_rect(clip_rect_window, self.world_bounds);

if response.dragged() {
self.world_to_view.translation += response.drag_delta();
world_to_view.translation += response.drag_delta();
}

let view_to_window = TSTransform::from_translation(ui.min_rect().left_top().to_vec2());
let world_to_window = view_to_window * self.world_to_view;

#[cfg(debug_assertions)]
if response.double_clicked() {
if let Some(window) = response.interact_pointer_pos() {
log::debug!(
"Click event! Window: {:?}, View: {:?} World: {:?}",
window,
view_to_window.inverse() * window,
world_to_window.inverse() * window,
);
}
}

let world_to_window = view_to_window * world_to_view;

if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
// Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered.
Expand All @@ -87,13 +70,13 @@ impl ViewBuilder {
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);

// Zoom in on pointer:
self.world_to_view = self.world_to_view
world_to_view = world_to_view
* TSTransform::from_translation(pointer_in_world.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_world.to_vec2());

// Pan:
self.world_to_view = TSTransform::from_translation(pan_delta) * self.world_to_view;
world_to_view = TSTransform::from_translation(pan_delta) * world_to_view;
}
}

Expand Down Expand Up @@ -137,7 +120,10 @@ impl ViewBuilder {
}
}

clip_rect_window
(
(view_to_window * world_to_view).inverse() * clip_rect_window,
response,
)
}
}

Expand Down
27 changes: 8 additions & 19 deletions crates/viewer/re_space_view_graph/src/ui/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,36 @@ use re_viewer_context::SpaceViewState;

use crate::graph::NodeIndex;

use super::{bounding_rect_from_iter, scene::ViewBuilder};
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.
#[derive(Default)]
pub struct GraphSpaceViewState {
pub viewer: ViewBuilder,

/// Indicates if the viewer should fit to the screen the next time it is rendered.
pub should_fit_to_screen: bool,

/// Positions of the nodes in world space.
pub layout: HashMap<NodeIndex, egui::Rect>,

pub visual_bounds_2d: Option<VisualBounds2D>,
pub show_debug: bool,

pub world_bounds: Option<VisualBounds2D>,
}

impl GraphSpaceViewState {
pub fn bounding_box_ui(&mut self, ui: &mut egui::Ui) {
ui.grid_left_hand_label("Bounding box")
.on_hover_text("The bounding box encompassing all Entities in the view right now");
pub fn layout_ui(&mut self, ui: &mut egui::Ui) {
ui.grid_left_hand_label("Layout")
.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);
let egui::Rect { min, max } = bounding_rect_from_iter(self.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()
{
self.should_fit_to_screen = true;
}
}

pub fn debug_ui(&mut self, ui: &mut egui::Ui) {
ui.re_checkbox(&mut self.viewer.show_debug, "Show debug information")
ui.re_checkbox(&mut self.show_debug, "Show debug information")
.on_hover_text("Shows debug information for the current graph");
ui.end_row();
}
Expand Down
61 changes: 48 additions & 13 deletions crates/viewer/re_space_view_graph/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@ use std::collections::HashSet;

use egui::{self, Rect};

use re_log::ResultExt as _;
use re_log_types::EntityPath;
use re_types::{components, SpaceViewClassIdentifier};
use re_ui::{self, UiExt};
use re_space_view::view_property_ui;
use re_types::{
blueprint::{self, archetypes::VisualBounds2D},
components, SpaceViewClassIdentifier,
};
use re_ui::{self, UiExt as _};
use re_viewer_context::{
external::re_entity_db::InstancePath, IdentifiedViewSystem as _, Item, SpaceViewClass,
SpaceViewClassLayoutPriority, SpaceViewClassRegistryError, SpaceViewId,
SpaceViewSpawnHeuristics, SpaceViewState, SpaceViewStateExt as _,
SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, SystemExecutionOutput, ViewQuery,
ViewerContext,
};
use re_viewport_blueprint::ViewProperty;

use crate::{
graph::{Graph, NodeIndex},
ui::{self, bounding_rect_from_iter, GraphSpaceViewState},
ui::{self, bounding_rect_from_iter, scene::ViewBuilder, GraphSpaceViewState},
visualizers::{EdgesVisualizer, NodeVisualizer},
};

Expand Down Expand Up @@ -59,7 +65,7 @@ impl SpaceViewClass for GraphSpaceView {
.downcast_ref::<GraphSpaceViewState>()
.ok()
.map(|state| {
let (width, height) = state.visual_bounds_2d.map_or_else(
let (width, height) = state.world_bounds.map_or_else(
|| {
let bbox = bounding_rect_from_iter(state.layout.values());
(
Expand Down Expand Up @@ -103,19 +109,21 @@ impl SpaceViewClass for GraphSpaceView {
/// In this sample we show a combo box to select the color coordinates mode.
fn selection_ui(
&self,
_ctx: &ViewerContext<'_>,
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
state: &mut dyn SpaceViewState,
_space_origin: &EntityPath,
_space_view_id: SpaceViewId,
space_view_id: SpaceViewId,
) -> Result<(), SpaceViewSystemExecutionError> {
let state = state.downcast_mut::<GraphSpaceViewState>()?;

ui.selection_grid("graph_settings_ui").show(ui, |ui| {
state.bounding_box_ui(ui);
ui.selection_grid("graph_view_settings_ui").show(ui, |ui| {
state.layout_ui(ui);
state.debug_ui(ui);
});

view_property_ui::<VisualBounds2D>(ctx, ui, space_view_id, self, state);

Ok(())
}

Expand Down Expand Up @@ -145,7 +153,28 @@ impl SpaceViewClass for GraphSpaceView {

let layout_was_empty = state.layout.is_empty();

state.viewer.scene(ui, |mut scene| {
let bounds_property = ViewProperty::from_archetype::<VisualBounds2D>(
ctx.blueprint_db(),
ctx.blueprint_query,
query.space_view_id,
);

let bounds: blueprint::components::VisualBounds2D = bounds_property
.component_or_fallback(ctx, self, state)
.ok_or_log_error()
.unwrap_or_default();

state.world_bounds = Some(bounds);
let bounds_rect: egui::Rect = bounds.into();

let mut viewer = ViewBuilder::from_world_bounds(bounds_rect);

// TODO(grtlr): Is there a blueprint archetype for debug information?
if state.show_debug {
viewer.show_debug();
}

let (new_world_bounds, response) = viewer.scene(ui, |mut scene| {
for data in &node_system.data {
let ent_highlight = query.highlights.entity_highlight(data.entity_path.hash());

Expand Down Expand Up @@ -215,13 +244,19 @@ impl SpaceViewClass for GraphSpaceView {
}
});

// Update blueprint if changed
let updated_bounds: blueprint::components::VisualBounds2D = new_world_bounds.into();
if response.double_clicked() || layout_was_empty {
bounds_property.reset_blueprint_component::<blueprint::components::VisualBounds2D>(ctx);
} else if bounds != updated_bounds {
bounds_property.save_blueprint_component(ctx, &updated_bounds);
}
// Update stored bounds on the state, so visualizers see an up-to-date value.
state.world_bounds = Some(bounds);

// Clean up the layout for nodes that are no longer present.
state.layout.retain(|k, _| seen.contains(k));

if layout_was_empty {
state.viewer.fit_to_screen();
}

Ok(())
}
}

0 comments on commit 81d556d

Please sign in to comment.