re_data_ui/
annotation_context.rs

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                // Color first, to keep subsequent rows of the same things aligned
36                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                // Color first, to keep subsequent rows of the same things aligned
79                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    // TODO(#3168): this needs to use the index of the keypoint to look up the correct
104    // class_id. For now we use `latest_at_component_quiet` to avoid the warning spam.
105
106    // TODO(grtlr): If there's several class ids we have no idea which one to use.
107    // This code uses the first one that shows up.
108    // We should search instead for a class id that is likely a sibling of the keypoint id.
109    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                        // TODO(jleibs): Helper to do this with caching somewhere
247                        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; // column spacing.
295
296    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()) // id
302        .column(Column::auto().clip(true).at_least(40.0)) // label
303        .column(Column::auto()); // color
304
305    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}