use nohash_hasher::{IntMap, IntSet};
use re_entity_db::{EntityDb, EntityTree};
use re_log_types::{EntityPath, ResolvedEntityPathFilter};
use re_types::{
blueprint::archetypes::{Background, NearClipPlane, VisualBounds2D},
View as _, ViewClassIdentifier,
};
use re_ui::{Help, UiExt as _};
use re_view::view_property_ui;
use re_viewer_context::{
RecommendedView, ViewClass, ViewClassRegistryError, ViewId, ViewQuery, ViewSpawnHeuristics,
ViewState, ViewStateExt as _, ViewSystemExecutionError, ViewerContext,
VisualizableFilterContext,
};
use crate::{
contexts::register_spatial_contexts,
heuristics::default_visualized_entities_for_visualizer_kind,
max_image_dimension_subscriber::{ImageTypes, MaxDimensions},
spatial_topology::{SpatialTopology, SubSpaceConnectionFlags},
ui::SpatialViewState,
view_kind::SpatialViewKind,
visualizers::register_2d_spatial_visualizers,
};
#[derive(Default)]
pub struct VisualizableFilterContext2D {
pub entities_in_main_2d_space: IntSet<EntityPath>,
pub reprojectable_3d_entities: IntSet<EntityPath>,
}
impl VisualizableFilterContext for VisualizableFilterContext2D {
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Default)]
pub struct SpatialView2D;
type ViewType = re_types::blueprint::views::Spatial2DView;
impl ViewClass for SpatialView2D {
fn identifier() -> ViewClassIdentifier {
ViewType::identifier()
}
fn display_name(&self) -> &'static str {
"2D"
}
fn icon(&self) -> &'static re_ui::Icon {
&re_ui::icons::VIEW_2D
}
fn help(&self, egui_ctx: &egui::Context) -> Help {
super::ui_2d::help(egui_ctx)
}
fn on_register(
&self,
system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>,
) -> Result<(), ViewClassRegistryError> {
crate::spatial_topology::SpatialTopologyStoreSubscriber::subscription_handle();
crate::transform_cache::TransformCacheStoreSubscriber::subscription_handle();
crate::max_image_dimension_subscriber::MaxImageDimensionsStoreSubscriber::subscription_handle();
register_spatial_contexts(system_registry)?;
register_2d_spatial_visualizers(system_registry)?;
Ok(())
}
fn new_state(&self) -> Box<dyn ViewState> {
Box::<SpatialViewState>::default()
}
fn preferred_tile_aspect_ratio(&self, state: &dyn ViewState) -> Option<f32> {
state.downcast_ref::<SpatialViewState>().ok().map(|state| {
let (width, height) = state.visual_bounds_2d.map_or_else(
|| {
let bbox = &state.bounding_boxes.smoothed;
(
(bbox.max.x - bbox.min.x).abs(),
(bbox.max.y - bbox.min.y).abs(),
)
},
|bounds| {
(
bounds.x_range.abs_len() as f32,
bounds.y_range.abs_len() as f32,
)
},
);
width / height
})
}
fn supports_visible_time_range(&self) -> bool {
true
}
fn layout_priority(&self) -> re_viewer_context::ViewClassLayoutPriority {
re_viewer_context::ViewClassLayoutPriority::High
}
fn recommended_root_for_entities(
&self,
entities: &IntSet<EntityPath>,
entity_db: &EntityDb,
) -> Option<EntityPath> {
let common_ancestor = EntityPath::common_ancestor_of(entities.iter());
SpatialTopology::access(&entity_db.store_id(), |topo| {
topo.subspace_for_entity(&common_ancestor).origin.clone()
})
}
fn visualizable_filter_context(
&self,
space_origin: &EntityPath,
entity_db: &re_entity_db::EntityDb,
) -> Box<dyn VisualizableFilterContext> {
re_tracing::profile_function!();
let context = SpatialTopology::access(&entity_db.store_id(), |topo| {
let primary_space = topo.subspace_for_entity(space_origin);
if !primary_space.supports_2d_content() {
return VisualizableFilterContext2D {
entities_in_main_2d_space: std::iter::once(space_origin.clone()).collect(),
reprojectable_3d_entities: Default::default(),
};
}
let reprojectable_3d_entities = if primary_space
.connection_to_parent
.contains(SubSpaceConnectionFlags::Pinhole)
{
topo.subspace_for_subspace_origin(primary_space.parent_space)
.map(|parent_space| parent_space.entities.clone())
.unwrap_or_default()
} else {
Default::default()
};
VisualizableFilterContext2D {
entities_in_main_2d_space: primary_space.entities.clone(),
reprojectable_3d_entities,
}
});
Box::new(context.unwrap_or_default())
}
fn spawn_heuristics(
&self,
ctx: &ViewerContext<'_>,
suggested_filter: &ResolvedEntityPathFilter,
) -> re_viewer_context::ViewSpawnHeuristics {
re_tracing::profile_function!();
let indicated_entities = default_visualized_entities_for_visualizer_kind(
ctx,
Self::identifier(),
SpatialViewKind::TwoD,
suggested_filter,
);
let image_dimensions =
crate::max_image_dimension_subscriber::MaxImageDimensionsStoreSubscriber::access(
&ctx.recording_id(),
|image_dimensions| image_dimensions.clone(),
)
.unwrap_or_default();
SpatialTopology::access(&ctx.recording_id(), |topo| {
ViewSpawnHeuristics::new(topo.iter_subspaces().flat_map(|subspace| {
if !subspace.supports_2d_content()
|| subspace.entities.is_empty()
|| indicated_entities.is_disjoint(&subspace.entities)
{
return Vec::new();
}
let relevant_entities: IntSet<EntityPath> = subspace
.entities
.iter()
.filter(|e| indicated_entities.contains(e))
.cloned()
.collect();
let recommended_root = if subspace
.connection_to_parent
.contains(SubSpaceConnectionFlags::Pinhole)
{
subspace.origin.clone()
} else {
EntityPath::common_ancestor_of(relevant_entities.iter())
};
let mut recommended_views = Vec::<RecommendedView>::new();
recommended_views_with_image_splits(
ctx,
&image_dimensions,
&recommended_root,
&relevant_entities,
&mut recommended_views,
);
if recommended_views.is_empty() {
recommended_views.push(RecommendedView::new_subtree(recommended_root));
}
recommended_views
}))
})
.unwrap_or_default()
}
fn selection_ui(
&self,
ctx: &re_viewer_context::ViewerContext<'_>,
ui: &mut egui::Ui,
state: &mut dyn ViewState,
_space_origin: &EntityPath,
view_id: ViewId,
) -> Result<(), ViewSystemExecutionError> {
let state = state.downcast_mut::<SpatialViewState>()?;
ui.selection_grid("spatial_settings_ui").show(ui, |ui| {
state.bounding_box_ui(ui, SpatialViewKind::TwoD);
});
re_ui::list_item::list_item_scope(ui, "spatial_view2d_selection_ui", |ui| {
view_property_ui::<VisualBounds2D>(ctx, ui, view_id, self, state);
view_property_ui::<NearClipPlane>(ctx, ui, view_id, self, state);
view_property_ui::<Background>(ctx, ui, view_id, self, state);
});
Ok(())
}
fn ui(
&self,
ctx: &ViewerContext<'_>,
ui: &mut egui::Ui,
state: &mut dyn ViewState,
query: &ViewQuery<'_>,
system_output: re_viewer_context::SystemExecutionOutput,
) -> Result<(), ViewSystemExecutionError> {
re_tracing::profile_function!();
let state = state.downcast_mut::<SpatialViewState>()?;
state.update_frame_statistics(ui, &system_output, SpatialViewKind::TwoD);
self.view_2d(ctx, ui, state, query, system_output)
}
}
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
struct NonNestedImageCounts {
color: usize,
depth: usize,
segmentation: usize,
}
impl NonNestedImageCounts {
fn total(&self) -> usize {
let Self {
color,
depth,
segmentation,
} = self;
color + depth + segmentation
}
fn has_any_images(&self) -> bool {
self.total() > 0
}
fn increment_count(&mut self, dims: &MaxDimensions) {
self.color += dims.image_types.contains(ImageTypes::IMAGE) as usize
+ dims.image_types.contains(ImageTypes::ENCODED_IMAGE) as usize
+ dims.image_types.contains(ImageTypes::VIDEO) as usize;
self.depth += dims.image_types.contains(ImageTypes::DEPTH_IMAGE) as usize;
self.segmentation += dims.image_types.contains(ImageTypes::SEGMENTATION_IMAGE) as usize;
}
}
fn has_single_shared_image_dimension(
image_dimensions: &IntMap<EntityPath, MaxDimensions>,
subtree: &EntityTree,
non_nested_image_counts: &mut NonNestedImageCounts,
) -> bool {
let mut image_dimension = None;
let mut pending_subtrees = vec![subtree];
while let Some(subtree) = pending_subtrees.pop() {
if let Some(dimensions) = image_dimensions.get(&subtree.path) {
let new_dimension = [dimensions.height, dimensions.width];
if let Some(existing_dimension) = image_dimension {
if existing_dimension != new_dimension {
return false;
}
} else {
image_dimension = Some(new_dimension);
}
non_nested_image_counts.increment_count(dimensions);
}
pending_subtrees.extend(subtree.children.values());
}
image_dimension.is_some()
}
fn recommended_views_with_image_splits(
ctx: &ViewerContext<'_>,
image_dimensions: &IntMap<EntityPath, MaxDimensions>,
recommended_origin: &EntityPath,
visualizable_entities: &IntSet<EntityPath>,
recommended: &mut Vec<RecommendedView>,
) {
re_tracing::profile_function!();
let tree = ctx.recording().tree();
let Some(subtree) = tree.subtree(recommended_origin) else {
if cfg!(debug_assertions) {
re_log::warn_once!("Ancestor of entity not found in entity tree.");
}
return;
};
let mut image_counts = NonNestedImageCounts::default();
let all_have_same_size =
has_single_shared_image_dimension(image_dimensions, subtree, &mut image_counts);
if !image_counts.has_any_images() {
return;
}
let could_have_subtree_view =
all_have_same_size && image_counts.color <= 1 && image_counts.depth <= 1;
if could_have_subtree_view && visualizable_entities.contains(recommended_origin) {
recommended.push(RecommendedView::new_subtree(recommended_origin.clone()));
return; }
let num_recommended_before = recommended.len();
if visualizable_entities.contains(recommended_origin) {
recommended.push(RecommendedView::new_single_entity(
recommended_origin.clone(),
));
}
for child in subtree.children.values() {
recommended_views_with_image_splits(
ctx,
image_dimensions,
&child.path,
visualizable_entities,
recommended,
);
}
let num_children_added = recommended.len() - num_recommended_before;
if could_have_subtree_view {
if num_children_added <= 1 {
} else {
recommended.truncate(num_recommended_before);
recommended.push(RecommendedView::new_subtree(recommended_origin.clone()));
}
}
}