use egui::{NumExt as _, Vec2, color_picker};
use itertools::Itertools as _;
use re_log_types::EntityPath;
use re_sdk_types::components::{self, AnnotationContext};
use re_sdk_types::datatypes::{
AnnotationInfo, ClassDescription, ClassDescriptionMapElem, KeypointId, KeypointPair,
};
use re_sdk_types::{Component as _, ComponentDescriptor, RowId};
use re_ui::UiExt as _;
use re_ui::syntax_highlighting::SyntaxHighlightedBuilder;
use re_viewer_context::{StoreViewContext, UiLayout, auto_color_egui};
use super::DataUi;
impl crate::EntityDataUi for components::ClassId {
fn entity_data_ui(
&self,
ctx: &StoreViewContext<'_>,
ui: &mut egui::Ui,
ui_layout: UiLayout,
entity_path: &EntityPath,
_component_descriptor: &ComponentDescriptor,
_row_id: Option<RowId>,
) {
let annotations = crate::annotations(ctx, entity_path);
let class = annotations
.resolved_class_description(Some(*self))
.class_description;
if let Some(class) = class {
let response = ui.horizontal(|ui| {
small_color_ui(ui, &class.info);
let mut text = format!("{}", self.0);
if let Some(label) = &class.info.label {
text.push(' ');
text.push_str(label.as_str());
}
ui_layout.label(ui, text);
});
let id = self.0;
if ui_layout.is_single_line() {
if !class.keypoint_connections.is_empty() || !class.keypoint_annotations.is_empty()
{
response.response.on_hover_ui(|ui| {
class_description_ui(ui, UiLayout::Tooltip, class, id);
});
}
} else {
ui.separator();
class_description_ui(ui, ui_layout, class, id);
}
} else {
ui_layout.label(ui, format!("{}", self.0));
}
}
}
impl crate::EntityDataUi for components::KeypointId {
fn entity_data_ui(
&self,
ctx: &StoreViewContext<'_>,
ui: &mut egui::Ui,
ui_layout: UiLayout,
entity_path: &EntityPath,
_component_descriptor: &ComponentDescriptor,
_row_id: Option<RowId>,
) {
if let Some(info) = annotation_info(ctx, entity_path, self.0) {
ui.horizontal(|ui| {
small_color_ui(ui, &info);
let mut builder = SyntaxHighlightedBuilder::new();
builder.append_index(&self.0.to_string());
if let Some(label) = &info.label {
builder.append_string_value(label);
}
ui_layout.data_label(ui, builder);
});
} else {
ui_layout.data_label(
ui,
SyntaxHighlightedBuilder::new().with_index(&self.0.to_string()),
);
}
}
}
fn annotation_info(
ctx: &StoreViewContext<'_>,
entity_path: &re_log_types::EntityPath,
keypoint_id: KeypointId,
) -> Option<AnnotationInfo> {
let storage_engine = ctx.db.storage_engine();
let store = storage_engine.store();
let mut possible_class_id_components = store
.schema()
.all_components_for_entity(entity_path)?
.iter()
.copied()
.filter(|component| {
let descriptor = store
.schema()
.entity_component_descriptor(entity_path, *component);
descriptor.is_some_and(|d| d.component_type == Some(components::ClassId::name()))
});
let picked_class_id_component = possible_class_id_components.next()?;
let (_, class_id) = ctx.db.latest_at_component_quiet::<components::ClassId>(
entity_path,
&ctx.query(),
picked_class_id_component,
)?;
let annotations = crate::annotations(ctx, entity_path);
let class = annotations.resolved_class_description(Some(class_id));
class.keypoint_map?.get(&keypoint_id).cloned()
}
impl DataUi for AnnotationContext {
fn data_ui(&self, _ctx: &StoreViewContext<'_>, ui: &mut egui::Ui, ui_layout: UiLayout) {
match ui_layout {
UiLayout::List | UiLayout::Tooltip => {
let text = if self.0.len() == 1 {
let descr = &self.0[0].class_description;
format!(
"One class containing {} keypoints and {} connections",
descr.keypoint_annotations.len(),
descr.keypoint_connections.len()
)
} else {
format!("{} classes", self.0.len())
};
ui_layout.label(ui, text);
}
UiLayout::SelectionPanel => {
ui.vertical(|ui| {
ui.maybe_collapsing_header(true, "Classes", true, |ui| {
let annotation_infos = self
.0
.iter()
.map(|class| &class.class_description.info)
.sorted_by_key(|info| info.id)
.collect_vec();
annotation_info_table_ui(ui, ui_layout, &annotation_infos);
});
for ClassDescriptionMapElem {
class_id,
class_description,
} in &self.0
{
class_description_ui(ui, ui_layout, class_description, *class_id);
}
});
}
}
}
}
fn class_description_ui(
ui: &mut egui::Ui,
ui_layout: UiLayout,
class: &ClassDescription,
id: re_sdk_types::datatypes::ClassId,
) {
if class.keypoint_connections.is_empty() && class.keypoint_annotations.is_empty() {
return;
}
re_tracing::profile_function!();
let tokens = ui.tokens();
let use_collapsible = ui_layout == UiLayout::SelectionPanel;
let table_style = re_ui::TableStyle::Dense;
let row_height = tokens.table_row_height(table_style);
if !class.keypoint_annotations.is_empty() {
ui.maybe_collapsing_header(
use_collapsible,
&format!("Keypoints Annotation for Class {}", id.0),
true,
|ui| {
let annotation_infos = class
.keypoint_annotations
.iter()
.sorted_by_key(|annotation| annotation.id)
.collect_vec();
ui.push_id(format!("keypoint_annotations_{}", id.0), |ui| {
annotation_info_table_ui(ui, ui_layout, &annotation_infos);
});
},
);
}
if !class.keypoint_connections.is_empty() {
ui.maybe_collapsing_header(
use_collapsible,
&format!("Keypoint Connections for Class {}", id.0),
true,
|ui| {
use egui_extras::Column;
let table = ui_layout
.table(ui)
.id_salt(("keypoints_connections", id))
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto().clip(true).at_least(40.0))
.column(Column::auto().clip(true).at_least(40.0));
table
.header(tokens.deprecated_table_header_height(), |mut header| {
re_ui::DesignTokens::setup_table_header(&mut header);
header.col(|ui| {
ui.strong("From");
});
header.col(|ui| {
ui.strong("To");
});
})
.body(|mut body| {
tokens.setup_table_body(&mut body, table_style);
let keypoint_map: ahash::HashMap<KeypointId, AnnotationInfo> = {
re_tracing::profile_scope!("build_annotation_map");
class
.keypoint_annotations
.iter()
.map(|kp| (kp.id.into(), kp.clone()))
.collect()
};
body.rows(row_height, class.keypoint_connections.len(), |mut row| {
let pair = &class.keypoint_connections[row.index()];
let KeypointPair {
keypoint0,
keypoint1,
} = pair;
for id in [keypoint0, keypoint1] {
row.col(|ui| {
ui.label(
keypoint_map
.get(id)
.and_then(|info| info.label.as_ref())
.map_or_else(
|| format!("id {}", id.0),
|label| label.to_string(),
),
);
});
}
});
});
},
);
}
}
fn annotation_info_table_ui(
ui: &mut egui::Ui,
ui_layout: UiLayout,
annotation_infos: &[&AnnotationInfo],
) {
re_tracing::profile_function!();
let tokens = ui.tokens();
let table_style = re_ui::TableStyle::Dense;
let row_height = tokens.table_row_height(table_style);
ui.spacing_mut().item_spacing.x = 20.0;
use egui_extras::Column;
let table = ui_layout
.table(ui)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto()) .column(Column::auto().clip(true).at_least(40.0)) .column(Column::auto());
table
.header(tokens.deprecated_table_header_height(), |mut header| {
re_ui::DesignTokens::setup_table_header(&mut header);
header.col(|ui| {
ui.strong("Class Id");
});
header.col(|ui| {
ui.strong("Label");
});
header.col(|ui| {
ui.strong("Color");
});
})
.body(|mut body| {
tokens.setup_table_body(&mut body, table_style);
body.rows(row_height, annotation_infos.len(), |mut row| {
let info = &annotation_infos[row.index()];
row.col(|ui| {
ui.label(info.id.to_string());
});
row.col(|ui| {
let label = if let Some(label) = &info.label {
label.as_str()
} else {
""
};
ui.label(label);
});
row.col(|ui| {
color_ui(ui, info, Vec2::new(64.0, row_height));
});
});
});
}
fn color_ui(ui: &mut egui::Ui, info: &AnnotationInfo, size: Vec2) {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
let color = info
.color
.map_or_else(|| auto_color_egui(info.id), |color| color.into());
color_picker::show_color(ui, color, size);
if info.color.is_none() {
ui.weak("(auto)")
.on_hover_text("Color chosen automatically, since it was not logged");
}
});
}
fn small_color_ui(ui: &mut egui::Ui, info: &AnnotationInfo) {
let tokens = ui.tokens();
let size = egui::Vec2::splat(
tokens
.table_row_height(re_ui::TableStyle::Dense)
.at_most(ui.available_height()),
);
let color = info
.color
.map_or_else(|| auto_color_egui(info.id), |color| color.into());
let response = color_picker::show_color(ui, color, size);
if info.color.is_none() {
response.on_hover_text("Color chosen automatically, since it was not logged");
}
}