Skip to content

Commit

Permalink
Introduce a mechanism for blueprint-provided defaults (#6537)
Browse files Browse the repository at this point in the history
### What
- Part of: #5067
- Introduces a new UI for setting default values to the selection panel
for views
- This works similarly to the component override UI but is generally
somewhat simpler
- Adds checks for the different query helpers to return default values.

There's quite a bit of annoying logic in here related to when and how we
figure out whether a component is actually empty, motivating:
#6536

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6537?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6537?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/6537)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)

To run all checks from `main`, comment on the PR with `@rerun-bot
full-check`.
  • Loading branch information
jleibs authored Jun 11, 2024
1 parent 63952fe commit 3695925
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 38 deletions.
11 changes: 11 additions & 0 deletions crates/re_query/src/range/results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,17 @@ impl<'a, T: 'static> RangeData<'a, T> {

start_index..end_index
}

pub fn is_empty(&self) -> bool {
if let Some(data) = self.data.as_ref() {
match data {
Data::Owned(data) => data.dyn_num_values() == 0,
Data::Cached(data) => data.num_values() == 0,
}
} else {
true
}
}
}

impl RangeComponentResults {
Expand Down
241 changes: 241 additions & 0 deletions crates/re_selection_panel/src/defaults_ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
use std::collections::{BTreeMap, BTreeSet};

use itertools::Itertools;

use re_data_store::LatestAtQuery;
use re_log_types::{DataCell, DataRow, EntityPath, RowId};
use re_types_core::ComponentName;
use re_ui::UiExt as _;
use re_viewer_context::{
blueprint_timeline, ComponentUiTypes, QueryContext, SystemCommand, SystemCommandSender as _,
ViewContext, ViewSystemIdentifier,
};
use re_viewport_blueprint::SpaceViewBlueprint;

pub fn defaults_ui(ctx: &ViewContext<'_>, space_view: &SpaceViewBlueprint, ui: &mut egui::Ui) {
let db = ctx.viewer_ctx.blueprint_db();
let query = ctx.viewer_ctx.blueprint_query;
let resolver = Default::default();

// Cleared components should act as unset, so we filter out everything that's empty,
// even if they are listed in `all_components`.
let active_defaults = ctx
.blueprint_db()
.store()
.all_components(&blueprint_timeline(), &space_view.defaults_path)
.unwrap_or_default()
.into_iter()
.filter(|c| {
db.query_caches()
.latest_at(db.store(), query, &space_view.defaults_path, [*c])
.components
.get(c)
.and_then(|data| data.resolved(&resolver).ok())
.map_or(false, |data| !data.is_empty())
})
.collect::<BTreeSet<_>>();

// It only makes sense to set defaults for components that are used by a system in the view.
let mut component_to_vis: BTreeMap<ComponentName, ViewSystemIdentifier> = Default::default();

// Accumulate the components across all visualizers and track which visualizer
// each component came from so we can use it for fallbacks later.
//
// If two visualizers have the same component, the first one wins.
// TODO(jleibs): We can do something fancier in the future such as presenting both
// options once we have a motivating use-case.
for (id, vis) in ctx.visualizer_collection.iter_with_identifiers() {
for component in vis.visualizer_query_info().queried {
component_to_vis.entry(component).or_insert_with(|| id);
}
}

add_new_default(
ctx,
query,
ui,
&component_to_vis,
&active_defaults,
&space_view.defaults_path,
);

let sorted_overrides = active_defaults.iter().sorted();

let query_context = QueryContext {
viewer_ctx: ctx.viewer_ctx,
target_entity_path: &space_view.defaults_path,
archetype_name: None,
query,
view_state: ctx.view_state,
view_ctx: Some(ctx),
};

re_ui::list_item::list_item_scope(ui, "defaults", |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
for component_name in sorted_overrides {
let Some(visualizer_identifier) = component_to_vis.get(component_name) else {
continue;
};
let Ok(visualizer) = ctx
.visualizer_collection
.get_by_identifier(*visualizer_identifier)
else {
re_log::warn!(
"Failed to resolve visualizer identifier {visualizer_identifier}, to a visualizer implementation"
);
continue;
};

// TODO(jleibs): We're already doing this query above as part of the filter. This is kind of silly to do it again.
// Change the structure to avoid this.
let component_data = db
.query_caches()
.latest_at(
db.store(),
query,
&space_view.defaults_path,
[*component_name],
)
.components
.get(component_name)
.cloned(); /* arc */

if let Some(component_data) = component_data {
let value_fn = |ui: &mut egui::Ui| {
ctx.viewer_ctx.component_ui_registry.singleline_edit_ui(
&query_context,
ui,
db,
&space_view.defaults_path,
*component_name,
&component_data,
visualizer.as_fallback_provider(),
);
};

ui.list_item()
.interactive(false)
.show_flat(
ui,
re_ui::list_item::PropertyContent::new(component_name.short_name())
.min_desired_width(150.0)
.action_button(&re_ui::icons::CLOSE, || {
ctx.save_empty_blueprint_component_by_name(
&space_view.defaults_path,
*component_name,
);
})
.value_fn(|ui, _| value_fn(ui)),
)
.on_hover_text(component_name.full_name());
}
}
});
}

#[allow(clippy::too_many_arguments)]
pub fn add_new_default(
ctx: &ViewContext<'_>,
query: &LatestAtQuery,
ui: &mut egui::Ui,
component_to_vis: &BTreeMap<ComponentName, ViewSystemIdentifier>,
active_overrides: &BTreeSet<ComponentName>,
defaults_path: &EntityPath,
) {
let remaining_components = component_to_vis
.keys()
.filter(|c| !active_overrides.contains(*c))
.collect::<Vec<_>>();

let enabled = !remaining_components.is_empty();

ui.add_enabled_ui(enabled, |ui| {
let mut opened = false;
let menu = ui
.menu_button("Add", |ui| {
opened = true;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);

let query_context = QueryContext {
viewer_ctx: ctx.viewer_ctx,
target_entity_path: defaults_path,
archetype_name: None,
query,
view_state: ctx.view_state,
view_ctx: Some(ctx),
};

// Present the option to add new components for each component that doesn't
// already have an active override.
for (component, viz) in component_to_vis {
if active_overrides.contains(component) {
continue;
}

// If there is no registered editor, don't let the user create an override
// TODO(andreas): Can only handle single line editors right now.
if !ctx
.viewer_ctx
.component_ui_registry
.registered_ui_types(*component)
.contains(ComponentUiTypes::SingleLineEditor)
{
continue;
}

if ui.button(component.short_name()).clicked() {
// We are creating a new override. We need to decide what initial value to give it.
// - First see if there's an existing splat in the recording.
// - Next see if visualizer system wants to provide a value.
// - Finally, fall back on the default value from the component registry.

// TODO(jleibs): Is this the right place for fallbacks to come from?
let Some(mut initial_data) = ctx
.visualizer_collection
.get_by_identifier(*viz)
.ok()
.and_then(|sys| {
sys.fallback_for(&query_context, *component)
.map(|fallback| DataCell::from_arrow(*component, fallback))
.ok()
})
else {
re_log::warn!("Could not identify an initial value for: {}", component);
return;
};

initial_data.compute_size_bytes();

match DataRow::from_cells(
RowId::new(),
ctx.blueprint_timepoint_for_writes(),
defaults_path.clone(),
[initial_data],
) {
Ok(row) => {
ctx.viewer_ctx.command_sender.send_system(
SystemCommand::UpdateBlueprint(
ctx.blueprint_db().store_id().clone(),
vec![row],
),
);
}
Err(err) => {
re_log::warn!(
"Failed to create DataRow for blueprint component: {}",
err
);
}
}

ui.close_menu();
}
}
})
.response
.on_disabled_hover_text("No additional components available.");
if !opened {
menu.on_hover_text("Choose a component to specify an override value.".to_owned());
}
});
}
1 change: 1 addition & 0 deletions crates/re_selection_panel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! The UI for the selection panel.
mod defaults_ui;
mod override_ui;
mod query_range_ui;
mod selection_history_ui;
Expand Down
26 changes: 18 additions & 8 deletions crates/re_selection_panel/src/selection_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ use re_viewport_blueprint::{
ui::show_add_space_view_or_container_modal, SpaceViewBlueprint, ViewportBlueprint,
};

use crate::override_ui::{override_ui, override_visualizer_ui};
use crate::space_view_entity_picker::SpaceViewEntityPicker;
use crate::{
defaults_ui::defaults_ui,
override_ui::{override_ui, override_visualizer_ui},
};
use crate::{
query_range_ui::query_range_ui_data_result, query_range_ui::query_range_ui_space_view,
selection_history_ui::SelectionHistoryUi,
Expand Down Expand Up @@ -237,13 +240,13 @@ impl SelectionPanel {
blueprint: &ViewportBlueprint,
view_states: &mut ViewStates,
ui: &mut Ui,
space_view_id: SpaceViewId,
view_id: SpaceViewId,
) {
if let Some(space_view) = blueprint.space_view(&space_view_id) {
if let Some(space_view) = blueprint.space_view(&view_id) {
if let Some(new_entity_path_filter) = self.entity_path_filter_ui(
ctx,
ui,
space_view_id,
view_id,
&space_view.contents.entity_path_filter,
&space_view.space_origin,
) {
Expand All @@ -262,7 +265,7 @@ impl SelectionPanel {
)
.clicked()
{
if let Some(new_space_view_id) = blueprint.duplicate_space_view(&space_view_id, ctx) {
if let Some(new_space_view_id) = blueprint.duplicate_space_view(&view_id, ctx) {
ctx.selection_state()
.set_selection(Item::SpaceView(new_space_view_id));
blueprint.mark_user_interaction(ctx);
Expand All @@ -273,10 +276,10 @@ impl SelectionPanel {
ui.full_span_separator();
ui.add_space(ui.spacing().item_spacing.y / 2.0);

if let Some(space_view) = blueprint.space_view(&space_view_id) {
if let Some(space_view) = blueprint.space_view(&view_id) {
let class_identifier = space_view.class_identifier();

let space_view_state = view_states.get_mut(
let view_state = view_states.get_mut(
ctx.space_view_class_registry,
space_view.id,
class_identifier,
Expand All @@ -293,7 +296,7 @@ impl SelectionPanel {
if let Err(err) = space_view_class.selection_ui(
ctx,
ui,
space_view_state.view_state.as_mut(),
view_state.view_state.as_mut(),
&space_view.space_origin,
space_view.id,
&mut props,
Expand All @@ -308,6 +311,13 @@ impl SelectionPanel {
if props_before != props {
space_view.save_legacy_properties(ctx, props);
}

let view_ctx =
space_view.bundle_context_with_state(ctx, view_state.view_state.as_ref());

ui.large_collapsing_header("Component Defaults", true, |ui| {
defaults_ui(&view_ctx, space_view, ui);
});
}
}

Expand Down
4 changes: 3 additions & 1 deletion crates/re_space_view/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ mod screenshot;
mod view_property_ui;

pub use heuristics::suggest_space_view_for_each_entity;
pub use query::{latest_at_with_overrides, range_with_overrides, DataResultQuery};
pub use query::{
latest_at_with_blueprint_resolved_data, range_with_blueprint_resolved_data, DataResultQuery,
};
pub use results_ext::{HybridResults, RangeResultsExt};
pub use screenshot::ScreenshotMode;
pub use view_property_ui::view_property_ui;
Expand Down
Loading

0 comments on commit 3695925

Please sign in to comment.