1use egui::{NumExt as _, Vec2, color_picker};
2use itertools::Itertools as _;
3use re_log_types::EntityPath;
4use re_types::{
5 Component as _, ComponentDescriptor, RowId,
6 components::{self, AnnotationContext},
7 datatypes::{
8 AnnotationInfo, ClassDescription, ClassDescriptionMapElem, KeypointId, KeypointPair,
9 },
10};
11use re_ui::UiExt as _;
12use re_ui::syntax_highlighting::SyntaxHighlightedBuilder;
13use re_viewer_context::{UiLayout, ViewerContext, auto_color_egui};
14
15use super::DataUi;
16
17impl crate::EntityDataUi for components::ClassId {
18 fn entity_data_ui(
19 &self,
20 ctx: &ViewerContext<'_>,
21 ui: &mut egui::Ui,
22 ui_layout: UiLayout,
23 entity_path: &EntityPath,
24 _component_descriptor: &ComponentDescriptor,
25 _row_id: Option<RowId>,
26 query: &re_chunk_store::LatestAtQuery,
27 _db: &re_entity_db::EntityDb,
28 ) {
29 let annotations = crate::annotations(ctx, query, entity_path);
30 let class = annotations
31 .resolved_class_description(Some(*self))
32 .class_description;
33 if let Some(class) = class {
34 let response = ui.horizontal(|ui| {
35 small_color_ui(ui, &class.info);
37 let mut text = format!("{}", self.0);
38 if let Some(label) = &class.info.label {
39 text.push(' ');
40 text.push_str(label.as_str());
41 }
42 ui_layout.label(ui, text);
43 });
44
45 let id = self.0;
46
47 if ui_layout.is_single_line() {
48 if !class.keypoint_connections.is_empty() || !class.keypoint_annotations.is_empty()
49 {
50 response.response.on_hover_ui(|ui| {
51 class_description_ui(ui, UiLayout::Tooltip, class, id);
52 });
53 }
54 } else {
55 ui.separator();
56 class_description_ui(ui, ui_layout, class, id);
57 }
58 } else {
59 ui_layout.label(ui, format!("{}", self.0));
60 }
61 }
62}
63
64impl crate::EntityDataUi for components::KeypointId {
65 fn entity_data_ui(
66 &self,
67 ctx: &ViewerContext<'_>,
68 ui: &mut egui::Ui,
69 ui_layout: UiLayout,
70 entity_path: &EntityPath,
71 _component_descriptor: &ComponentDescriptor,
72 _row_id: Option<RowId>,
73 query: &re_chunk_store::LatestAtQuery,
74 _db: &re_entity_db::EntityDb,
75 ) {
76 if let Some(info) = annotation_info(ctx, entity_path, query, self.0) {
77 ui.horizontal(|ui| {
78 small_color_ui(ui, &info);
80 let mut builder = SyntaxHighlightedBuilder::new();
81 builder.append_index(&self.0.to_string());
82 if let Some(label) = &info.label {
83 builder.append_string_value(label);
84 }
85
86 ui_layout.data_label(ui, builder);
87 });
88 } else {
89 ui_layout.data_label(
90 ui,
91 SyntaxHighlightedBuilder::new().with_index(&self.0.to_string()),
92 );
93 }
94 }
95}
96
97fn annotation_info(
98 ctx: &re_viewer_context::ViewerContext<'_>,
99 entity_path: &re_log_types::EntityPath,
100 query: &re_chunk_store::LatestAtQuery,
101 keypoint_id: KeypointId,
102) -> Option<AnnotationInfo> {
103 let storage_engine = ctx.recording().storage_engine();
110 let store = storage_engine.store();
111 let mut possible_class_id_components = store
112 .all_components_for_entity(entity_path)?
113 .into_iter()
114 .filter(|component| {
115 let descriptor = store.entity_component_descriptor(entity_path, *component);
116 descriptor.is_some_and(|d| d.component_type == Some(components::ClassId::name()))
117 });
118 let picked_class_id_component = possible_class_id_components.next()?;
119
120 let (_, class_id) = ctx
121 .recording()
122 .latest_at_component_quiet::<components::ClassId>(
123 entity_path,
124 query,
125 picked_class_id_component,
126 )?;
127
128 let annotations = crate::annotations(ctx, query, entity_path);
129 let class = annotations.resolved_class_description(Some(class_id));
130 class.keypoint_map?.get(&keypoint_id).cloned()
131}
132
133impl DataUi for AnnotationContext {
134 fn data_ui(
135 &self,
136 _ctx: &ViewerContext<'_>,
137 ui: &mut egui::Ui,
138 ui_layout: UiLayout,
139 _query: &re_chunk_store::LatestAtQuery,
140 _db: &re_entity_db::EntityDb,
141 ) {
142 match ui_layout {
143 UiLayout::List | UiLayout::Tooltip => {
144 let text = if self.0.len() == 1 {
145 let descr = &self.0[0].class_description;
146
147 format!(
148 "One class containing {} keypoints and {} connections",
149 descr.keypoint_annotations.len(),
150 descr.keypoint_connections.len()
151 )
152 } else {
153 format!("{} classes", self.0.len())
154 };
155 ui_layout.label(ui, text);
156 }
157 UiLayout::SelectionPanel => {
158 ui.vertical(|ui| {
159 ui.maybe_collapsing_header(true, "Classes", true, |ui| {
160 let annotation_infos = self
161 .0
162 .iter()
163 .map(|class| &class.class_description.info)
164 .sorted_by_key(|info| info.id)
165 .collect_vec();
166 annotation_info_table_ui(ui, ui_layout, &annotation_infos);
167 });
168
169 for ClassDescriptionMapElem {
170 class_id,
171 class_description,
172 } in &self.0
173 {
174 class_description_ui(ui, ui_layout, class_description, *class_id);
175 }
176 });
177 }
178 }
179 }
180}
181
182fn class_description_ui(
183 ui: &mut egui::Ui,
184 ui_layout: UiLayout,
185 class: &ClassDescription,
186 id: re_types::datatypes::ClassId,
187) {
188 if class.keypoint_connections.is_empty() && class.keypoint_annotations.is_empty() {
189 return;
190 }
191
192 re_tracing::profile_function!();
193
194 let tokens = ui.tokens();
195
196 let use_collapsible = ui_layout == UiLayout::SelectionPanel;
197
198 let table_style = re_ui::TableStyle::Dense;
199
200 let row_height = tokens.table_row_height(table_style);
201 if !class.keypoint_annotations.is_empty() {
202 ui.maybe_collapsing_header(
203 use_collapsible,
204 &format!("Keypoints Annotation for Class {}", id.0),
205 true,
206 |ui| {
207 let annotation_infos = class
208 .keypoint_annotations
209 .iter()
210 .sorted_by_key(|annotation| annotation.id)
211 .collect_vec();
212 ui.push_id(format!("keypoint_annotations_{}", id.0), |ui| {
213 annotation_info_table_ui(ui, ui_layout, &annotation_infos);
214 });
215 },
216 );
217 }
218
219 if !class.keypoint_connections.is_empty() {
220 ui.maybe_collapsing_header(
221 use_collapsible,
222 &format!("Keypoint Connections for Class {}", id.0),
223 true,
224 |ui| {
225 use egui_extras::Column;
226
227 let table = ui_layout
228 .table(ui)
229 .id_salt(("keypoints_connections", id))
230 .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
231 .column(Column::auto().clip(true).at_least(40.0))
232 .column(Column::auto().clip(true).at_least(40.0));
233 table
234 .header(tokens.deprecated_table_header_height(), |mut header| {
235 re_ui::DesignTokens::setup_table_header(&mut header);
236 header.col(|ui| {
237 ui.strong("From");
238 });
239 header.col(|ui| {
240 ui.strong("To");
241 });
242 })
243 .body(|mut body| {
244 tokens.setup_table_body(&mut body, table_style);
245
246 let keypoint_map: ahash::HashMap<KeypointId, AnnotationInfo> = {
248 re_tracing::profile_scope!("build_annotation_map");
249 class
250 .keypoint_annotations
251 .iter()
252 .map(|kp| (kp.id.into(), kp.clone()))
253 .collect()
254 };
255
256 body.rows(row_height, class.keypoint_connections.len(), |mut row| {
257 let pair = &class.keypoint_connections[row.index()];
258 let KeypointPair {
259 keypoint0,
260 keypoint1,
261 } = pair;
262
263 for id in [keypoint0, keypoint1] {
264 row.col(|ui| {
265 ui.label(
266 keypoint_map
267 .get(id)
268 .and_then(|info| info.label.as_ref())
269 .map_or_else(
270 || format!("id {}", id.0),
271 |label| label.to_string(),
272 ),
273 );
274 });
275 }
276 });
277 });
278 },
279 );
280 }
281}
282
283fn annotation_info_table_ui(
284 ui: &mut egui::Ui,
285 ui_layout: UiLayout,
286 annotation_infos: &[&AnnotationInfo],
287) {
288 re_tracing::profile_function!();
289
290 let tokens = ui.tokens();
291 let table_style = re_ui::TableStyle::Dense;
292 let row_height = tokens.table_row_height(table_style);
293
294 ui.spacing_mut().item_spacing.x = 20.0; use egui_extras::Column;
297
298 let table = ui_layout
299 .table(ui)
300 .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
301 .column(Column::auto()) .column(Column::auto().clip(true).at_least(40.0)) .column(Column::auto()); table
306 .header(tokens.deprecated_table_header_height(), |mut header| {
307 re_ui::DesignTokens::setup_table_header(&mut header);
308 header.col(|ui| {
309 ui.strong("Class Id");
310 });
311 header.col(|ui| {
312 ui.strong("Label");
313 });
314 header.col(|ui| {
315 ui.strong("Color");
316 });
317 })
318 .body(|mut body| {
319 tokens.setup_table_body(&mut body, table_style);
320
321 body.rows(row_height, annotation_infos.len(), |mut row| {
322 let info = &annotation_infos[row.index()];
323 row.col(|ui| {
324 ui.label(info.id.to_string());
325 });
326 row.col(|ui| {
327 let label = if let Some(label) = &info.label {
328 label.as_str()
329 } else {
330 ""
331 };
332 ui.label(label);
333 });
334 row.col(|ui| {
335 color_ui(ui, info, Vec2::new(64.0, row_height));
336 });
337 });
338 });
339}
340
341fn color_ui(ui: &mut egui::Ui, info: &AnnotationInfo, size: Vec2) {
342 ui.horizontal(|ui| {
343 ui.spacing_mut().item_spacing.x = 8.0;
344 let color = info
345 .color
346 .map_or_else(|| auto_color_egui(info.id), |color| color.into());
347 color_picker::show_color(ui, color, size);
348 if info.color.is_none() {
349 ui.weak("(auto)")
350 .on_hover_text("Color chosen automatically, since it was not logged");
351 }
352 });
353}
354
355fn small_color_ui(ui: &mut egui::Ui, info: &AnnotationInfo) {
356 let tokens = ui.tokens();
357 let size = egui::Vec2::splat(
358 tokens
359 .table_row_height(re_ui::TableStyle::Dense)
360 .at_most(ui.available_height()),
361 );
362
363 let color = info
364 .color
365 .map_or_else(|| auto_color_egui(info.id), |color| color.into());
366
367 let response = color_picker::show_color(ui, color, size);
368
369 if info.color.is_none() {
370 response.on_hover_text("Color chosen automatically, since it was not logged");
371 }
372}