use std::collections::BTreeMap;
use itertools::Itertools as _;
use nohash_hasher::IntSet;
use re_chunk_store::MissingChunkReporter;
use re_log_types::{ComponentPath, EntityPath};
use re_sdk_types::ViewClassIdentifier;
use vec1::Vec1;
use super::{ViewContext, ViewerDiagnostic};
use crate::{
DragAndDropFeedback, IndicatedEntities, PerVisualizerType, QueryRange, RecommendedMappings,
SystemExecutionOutput, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics,
ViewSystemExecutionError, ViewSystemIdentifier, ViewSystemRegistrator, ViewerContext,
VisualizableReason,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Ord, Eq)]
pub enum ViewClassLayoutPriority {
Low,
#[default]
Medium,
High,
}
pub struct RecommendedVisualizers {
all_recommendations: BTreeMap<ViewSystemIdentifier, Vec1<RecommendedMappings>>,
auto_spawned: BTreeMap<ViewSystemIdentifier, Vec1<RecommendedMappings>>,
}
impl RecommendedVisualizers {
pub fn new(
recommended_and_auto_spawned: BTreeMap<ViewSystemIdentifier, Vec1<RecommendedMappings>>,
) -> Self {
Self {
auto_spawned: recommended_and_auto_spawned.clone(),
all_recommendations: recommended_and_auto_spawned,
}
}
pub fn empty() -> Self {
Self::new(Default::default())
}
pub fn default(visualizer: ViewSystemIdentifier) -> Self {
Self::new(std::iter::once((visualizer, Default::default())).collect())
}
pub fn default_many(visualizers: impl IntoIterator<Item = ViewSystemIdentifier>) -> Self {
let recommended = visualizers
.into_iter()
.map(|v| (v, Default::default()))
.collect();
Self::new(recommended)
}
pub fn insert(
&mut self,
visualizer: ViewSystemIdentifier,
mappings: Vec1<RecommendedMappings>,
auto_spawn: bool,
) {
if auto_spawn {
Self::extend_mappings_unique(&mut self.auto_spawned, visualizer, mappings.clone());
}
Self::extend_mappings_unique(&mut self.all_recommendations, visualizer, mappings);
}
pub fn all_recommendations(
&self,
) -> &BTreeMap<ViewSystemIdentifier, Vec1<RecommendedMappings>> {
&self.all_recommendations
}
pub fn into_auto_spawned(self) -> BTreeMap<ViewSystemIdentifier, Vec1<RecommendedMappings>> {
self.auto_spawned
}
fn extend_mappings_unique(
map: &mut BTreeMap<ViewSystemIdentifier, Vec1<RecommendedMappings>>,
visualizer: ViewSystemIdentifier,
mappings: Vec1<RecommendedMappings>,
) {
match map.entry(visualizer) {
std::collections::btree_map::Entry::Occupied(mut entry) => {
let existing = entry.get_mut();
existing.extend(mappings);
*existing = Vec1::try_from_vec(existing.iter().cloned().unique().collect())
.expect("There was already at least one mapping.");
}
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(mappings);
}
}
}
}
pub type VisualizersSectionUi<'a> = Box<dyn Fn(&mut egui::Ui, &ViewContext<'_>) + 'a>;
pub struct VisualizersSectionOutput<'a> {
pub ui: VisualizersSectionUi<'a>,
pub add_options: Vec<(EntityPath, RecommendedVisualizers)>,
}
pub type SelectionSectionUi<'a> = Box<dyn Fn(&mut egui::Ui, &ViewContext<'_>) + 'a>;
pub type BlueprintPropertiesUi<'a> =
Box<dyn Fn(&mut egui::Ui, &ViewContext<'_>) -> Result<(), ViewSystemExecutionError> + 'a>;
pub struct SelectionSection<'a> {
pub title: String,
pub ui: SelectionSectionUi<'a>,
}
pub struct ViewSelectionUi<'a> {
pub show_entity_filter: bool,
pub visualizers: Option<VisualizersSectionOutput<'a>>,
pub extra_sections: Vec<SelectionSection<'a>>,
pub blueprint_properties: Option<BlueprintPropertiesUi<'a>>,
}
impl<'a> ViewSelectionUi<'a> {
pub fn properties_ui(
ui: impl Fn(&mut egui::Ui, &ViewContext<'_>) -> Result<(), ViewSystemExecutionError> + 'a,
) -> Self {
Self {
blueprint_properties: Some(Box::new(ui)),
..Default::default()
}
}
}
impl Default for ViewSelectionUi<'_> {
fn default() -> Self {
Self {
show_entity_filter: true,
visualizers: None,
extra_sections: Vec::new(),
blueprint_properties: None,
}
}
}
#[derive(Default)]
pub struct ViewClassUiOutput {
pub reports: Vec<ViewerDiagnostic>,
}
impl ViewClassUiOutput {
pub fn with_report(mut self, report: ViewerDiagnostic) -> Self {
self.reports.push(report);
self
}
}
pub trait ViewClass: Send + Sync {
fn identifier() -> ViewClassIdentifier
where
Self: Sized;
fn recommendation_order(&self) -> i32 {
0
}
fn display_name(&self) -> &'static str;
fn icon(&self) -> &'static re_ui::Icon {
&re_ui::icons::VIEW_GENERIC
}
fn help(&self, os: egui::os::OperatingSystem) -> re_ui::Help;
fn on_register(
&self,
system_registry: &mut ViewSystemRegistrator<'_>,
) -> Result<(), ViewClassRegistryError>;
fn new_state(&self) -> Box<dyn ViewState>;
fn preferred_tile_aspect_ratio(&self, _state: &dyn ViewState) -> Option<f32> {
None
}
fn layout_priority(&self) -> ViewClassLayoutPriority;
fn supports_visible_time_range(&self) -> bool {
false
}
fn default_query_range(&self, _state: &dyn ViewState) -> QueryRange {
QueryRange::LatestAt
}
fn recommended_origin_for_entities(
&self,
entities: &IntSet<EntityPath>,
_entity_db: &re_entity_db::EntityDb,
) -> Option<EntityPath> {
Some(EntityPath::common_ancestor_of(entities.iter()))
}
fn recommended_visualizers_for_entity(
&self,
entity_path: &EntityPath,
visualizers_with_reason: &[(ViewSystemIdentifier, &VisualizableReason)],
indicated_entities_per_visualizer: &PerVisualizerType<&IndicatedEntities>,
) -> RecommendedVisualizers {
let mut recommended = RecommendedVisualizers::empty();
for (visualizer, reason) in visualizers_with_reason {
if indicated_entities_per_visualizer
.get(visualizer)
.is_some_and(|matching_list| matching_list.contains(entity_path))
{
recommended.insert(*visualizer, reason.recommended_mappings(), true);
}
}
recommended
}
fn spawn_heuristics(
&self,
ctx: &ViewerContext<'_>,
include_entity: &dyn Fn(&EntityPath) -> bool,
) -> ViewSpawnHeuristics;
#[doc(alias = "settings_ui")]
fn selection_ui<'a>(&'a self, _ctx: &ViewContext<'_>) -> ViewSelectionUi<'a> {
ViewSelectionUi::default()
}
fn extra_title_bar_ui(
&self,
_ctx: &ViewerContext<'_>,
_ui: &mut egui::Ui,
_state: &mut dyn ViewState,
_space_origin: &EntityPath,
_view_id: ViewId,
) -> Result<(), ViewSystemExecutionError> {
Ok(())
}
fn reject_entity_drop_reason(
&self,
_ctx: &ViewerContext<'_>,
_entity_path: &EntityPath,
) -> Option<&'static str> {
None
}
fn handle_component_drop(
&self,
_ctx: &ViewerContext<'_>,
_view_id: ViewId,
_component_paths: &[ComponentPath],
_released: bool,
) -> DragAndDropFeedback {
DragAndDropFeedback::Ignore
}
#[doc(alias = "paint")]
#[doc(alias = "render")]
fn ui(
&self,
ctx: &ViewerContext<'_>,
missing_chunk_reporter: &MissingChunkReporter,
ui: &mut egui::Ui,
state: &mut dyn ViewState,
query: &ViewQuery<'_>,
system_output: SystemExecutionOutput,
) -> Result<ViewClassUiOutput, ViewSystemExecutionError>;
}
pub trait ViewClassExt<'a>: ViewClass + 'a {
fn view_context<'b>(
&self,
viewer_ctx: &'b ViewerContext<'b>,
view_id: ViewId,
view_state: &'b dyn ViewState,
space_origin: &'b EntityPath,
) -> ViewContext<'b>;
}
impl<'a, T> ViewClassExt<'a> for T
where
T: ViewClass + 'a,
{
fn view_context<'b>(
&self,
viewer_ctx: &'b ViewerContext<'b>,
view_id: ViewId,
view_state: &'b dyn ViewState,
space_origin: &'b EntityPath,
) -> ViewContext<'b> {
ViewContext {
viewer_ctx,
view_id,
view_class_identifier: T::identifier(),
space_origin,
view_state,
query_result: viewer_ctx.lookup_query_result(view_id),
}
}
}
pub trait ViewState: std::any::Any + Sync + Send {
fn as_any(&self) -> &dyn std::any::Any;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
fn heap_size_bytes(&self) -> u64;
}
impl re_byte_size::SizeBytes for dyn ViewState {
#[inline]
fn heap_size_bytes(&self) -> u64 {
ViewState::heap_size_bytes(self)
}
}
impl ViewState for () {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn heap_size_bytes(&self) -> u64 {
0
}
}
pub trait ViewStateExt: ViewState {
fn downcast_ref<T: ViewState>(&self) -> Result<&T, ViewSystemExecutionError> {
self.as_any()
.downcast_ref()
.ok_or(ViewSystemExecutionError::StateCastError(
std::any::type_name::<T>(),
))
}
fn downcast_mut<T: ViewState>(&mut self) -> Result<&mut T, ViewSystemExecutionError> {
self.as_any_mut()
.downcast_mut()
.ok_or(ViewSystemExecutionError::StateCastError(
std::any::type_name::<T>(),
))
}
}
impl ViewStateExt for dyn ViewState {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ComponentMatch, SingleRequiredComponentMatch, ViewClassPlaceholder,
VisualizerComponentSource,
};
use nohash_hasher::IntMap;
#[test]
fn native_mappings_take_precedence_over_ordered_heuristics() {
let entity = EntityPath::from("entity");
let visualizer: ViewSystemIdentifier = "TestVisualizer".into();
let target = "target".into();
let indicated = IndicatedEntities(std::iter::once(entity.clone()).collect());
let indicated_per_visualizer =
PerVisualizerType(std::iter::once((visualizer, &indicated)).collect());
let expected: Vec<_> = ["message_a", "message_b"]
.map(|source| {
RecommendedMappings::new(
target,
VisualizerComponentSource::SourceComponent {
source_component: source.into(),
selector: ".data".into(),
},
)
})
.into();
for native_source in [None, Some("other"), Some("target")] {
for sources in [["message_b", "message_a"], ["message_a", "message_b"]] {
let mut matches = IntMap::default();
for source in sources {
matches.insert(
source.into(),
ComponentMatch::HeuristicMapping {
component_type: None,
mappings: RecommendedMappings::new(
target,
VisualizerComponentSource::SourceComponent {
source_component: source.into(),
selector: ".data".into(),
},
),
},
);
}
if let Some(source) = native_source {
matches.insert(
source.into(),
ComponentMatch::NativeSemantics {
arrow_data_type: arrow::datatypes::DataType::Float64,
component_type: None,
},
);
}
let reason = VisualizableReason::SingleRequiredComponentMatch(
SingleRequiredComponentMatch {
target_component: target,
matches,
},
);
let recommended = ViewClassPlaceholder.recommended_visualizers_for_entity(
&entity,
&[(visualizer, &reason)],
&indicated_per_visualizer,
);
let native = native_source.map(|source| {
if source == "target" {
RecommendedMappings::default()
} else {
RecommendedMappings::new(
target,
VisualizerComponentSource::simple_map(source.into()),
)
}
});
let defaults = native.map_or_else(|| expected.clone(), |mapping| vec![mapping]);
assert_eq!(
recommended.all_recommendations()[&visualizer].as_slice(),
defaults,
"{native_source:?}, {sources:?}"
);
assert_eq!(
recommended.into_auto_spawned()[&visualizer].as_slice(),
defaults
);
let not_indicated = ViewClassPlaceholder.recommended_visualizers_for_entity(
&entity,
&[(visualizer, &reason)],
&Default::default(),
);
assert!(not_indicated.all_recommendations().is_empty());
}
}
}
}