Skip to content

Commit

Permalink
Multiple instances of points/arrows/boxes with single label display l…
Browse files Browse the repository at this point in the history
…abel now at the center (#6741)

### What

Previously, single labels would splat out, quickly hitting the "can't
display more labels" limit (described in #3465).
Now, there's a special meaning for a single labels: Instead of splatting
it across all instances, we place the single label at the center.

This is particularly useful with in-viewer overrides, as it allows to
set labels on objects with many instances.

![image](https://github.com/rerun-io/rerun/assets/1220815/a4e2462f-3fee-4cca-a4bd-d05cff263621)

A drawback of bounding box based placement is that the label moves with
the box. But I reckon it's a lot better than the alternative.

https://github.com/rerun-io/rerun/assets/1220815/866058a4-8edf-4ce7-9cd5-63f8b00e34a3

* Part of #6595

### Checklist

* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6741?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6741?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/6741)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)

To run all checks from `main`, comment on the PR with `@rerun-bot
full-check`.
  • Loading branch information
Wumpf authored Jul 4, 2024
1 parent 29d9528 commit 9137e49
Show file tree
Hide file tree
Showing 50 changed files with 598 additions and 497 deletions.
109 changes: 41 additions & 68 deletions crates/re_space_view_spatial/src/visualizers/arrows2d.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use re_entity_db::{EntityPath, InstancePathHash};
use re_log_types::Instance;
use re_query::range_zip_1x6;
use re_renderer::{renderer::LineStripFlags, LineDrawableBuilder, PickingLayerInstanceId};
Expand All @@ -8,34 +7,31 @@ use re_types::{
};
use re_viewer_context::{
auto_color_for_entity_path, ApplicableEntities, IdentifiedViewSystem, QueryContext,
ResolvedAnnotationInfos, SpaceViewSystemExecutionError, TypedComponentFallbackProvider,
ViewContext, ViewContextCollection, ViewQuery, VisualizableEntities, VisualizableFilterContext,
SpaceViewSystemExecutionError, TypedComponentFallbackProvider, ViewContext,
ViewContextCollection, ViewQuery, VisualizableEntities, VisualizableFilterContext,
VisualizerQueryInfo, VisualizerSystem,
};

use crate::{
contexts::SpatialSceneEntityContext,
view_kind::SpatialSpaceViewKind,
visualizers::{filter_visualizable_2d_entities, UiLabel, UiLabelTarget},
contexts::SpatialSceneEntityContext, view_kind::SpatialSpaceViewKind,
visualizers::filter_visualizable_2d_entities,
};

use super::{
entity_iterator::clamped, process_annotation_and_keypoint_slices, process_color_slice,
process_radius_slice, SpatialViewVisualizerData, SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES,
process_labels_2d, process_radius_slice, SpatialViewVisualizerData,
SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES,
};

// ---

pub struct Arrows2DVisualizer {
/// If the number of arrows in the batch is > max_labels, don't render point labels.
pub max_labels: usize,
pub data: SpatialViewVisualizerData,
}

impl Default for Arrows2DVisualizer {
fn default() -> Self {
Self {
max_labels: 10,
data: SpatialViewVisualizerData::new(Some(SpatialSpaceViewKind::TwoD)),
}
}
Expand All @@ -44,45 +40,6 @@ impl Default for Arrows2DVisualizer {
// NOTE: Do not put profile scopes in these methods. They are called for all entities and all
// timestamps within a time range -- it's _a lot_.
impl Arrows2DVisualizer {
fn process_labels<'a>(
entity_path: &'a EntityPath,
vectors: &'a [Vector2D],
origins: impl Iterator<Item = &'a Position2D> + 'a,
labels: &'a [Text],
colors: &'a [egui::Color32],
annotation_infos: &'a ResolvedAnnotationInfos,
world_from_obj: glam::Affine3A,
) -> impl Iterator<Item = UiLabel> + 'a {
let labels = clamped(labels, vectors.len());
let origins = origins.chain(std::iter::repeat(&Position2D::ZERO));
itertools::izip!(annotation_infos.iter(), vectors, origins, labels, colors)
.enumerate()
.filter_map(
move |(i, (annotation_info, vector, origin, label, color))| {
let label = annotation_info.label(Some(label.as_str()));
match (vector, label) {
(vector, Some(label)) => {
let midpoint =
// `0.45` rather than `0.5` to account for cap and such
glam::Vec2::from(origin.0) + glam::Vec2::from(vector.0) * 0.45;
let midpoint = world_from_obj.transform_point3(midpoint.extend(0.0));

Some(UiLabel {
text: label,
color: *color,
target: UiLabelTarget::Point2D(egui::pos2(midpoint.x, midpoint.y)),
labeled_instance: InstancePathHash::instance(
entity_path,
Instance::from(i as u64),
),
})
}
_ => None,
}
},
)
}

fn process_data<'a>(
&mut self,
ctx: &QueryContext<'_>,
Expand Down Expand Up @@ -115,31 +72,18 @@ impl Arrows2DVisualizer {
let colors =
process_color_slice(ctx, self, num_instances, &annotation_infos, data.colors);

if num_instances <= self.max_labels {
let origins = clamped(data.origins, num_instances);
self.data.ui_labels.extend(Self::process_labels(
entity_path,
data.vectors,
origins,
data.labels,
&colors,
&annotation_infos,
ent_context.world_from_entity,
));
}

let mut line_batch = line_builder
.batch(entity_path.to_string())
.world_from_obj(ent_context.world_from_entity)
.outline_mask_ids(ent_context.highlight.overall)
.picking_object_id(re_renderer::PickingLayerObjectId(entity_path.hash64()));

let mut bounding_box = macaw::BoundingBox::nothing();
let mut obj_space_bounding_box = macaw::BoundingBox::nothing();

let origins =
clamped(data.origins, num_instances).chain(std::iter::repeat(&Position2D::ZERO));
for (i, (vector, origin, radius, color)) in
itertools::izip!(data.vectors, origins, radii, colors).enumerate()
for (i, (vector, origin, radius, &color)) in
itertools::izip!(data.vectors, origins, radii, &colors).enumerate()
{
let vector: glam::Vec2 = vector.0.into();
let origin: glam::Vec2 = origin.0.into();
Expand All @@ -164,15 +108,44 @@ impl Arrows2DVisualizer {
segment.outline_mask_ids(*outline_mask_ids);
}

bounding_box.extend(origin.extend(0.0));
bounding_box.extend(end.extend(0.0));
obj_space_bounding_box.extend(origin.extend(0.0));
obj_space_bounding_box.extend(end.extend(0.0));
}

self.data.add_bounding_box(
entity_path.hash(),
bounding_box,
obj_space_bounding_box,
ent_context.world_from_entity,
);

if data.labels.len() == 1 || num_instances <= super::MAX_NUM_LABELS_PER_ENTITY {
// If there's many arrows but only a single label, place the single label at the middle of the visualization.
let label_positions = if data.labels.len() == 1 && num_instances > 1 {
// TODO(andreas): A smoothed over time (+ discontinuity detection) bounding box would be great.
itertools::Either::Left(std::iter::once(
obj_space_bounding_box.center().truncate(),
))
} else {
// Take middle point of every arrow.
let origins = clamped(data.origins, num_instances)
.chain(std::iter::repeat(&Position2D::ZERO));
itertools::Either::Right(data.vectors.iter().zip(origins).map(
|(vector, origin)| {
// `0.45` rather than `0.5` to account for cap and such
(glam::Vec2::from(origin.0) + glam::Vec2::from(vector.0)) * 0.45
},
))
};

self.data.ui_labels.extend(process_labels_2d(
entity_path,
label_positions,
data.labels,
&colors,
&annotation_infos,
ent_context.world_from_entity,
));
}
}
}
}
Expand Down
110 changes: 40 additions & 70 deletions crates/re_space_view_spatial/src/visualizers/arrows3d.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use re_entity_db::{EntityPath, InstancePathHash};
use re_log_types::Instance;
use re_query::range_zip_1x6;
use re_renderer::{renderer::LineStripFlags, LineDrawableBuilder, PickingLayerInstanceId};
Expand All @@ -8,34 +7,31 @@ use re_types::{
};
use re_viewer_context::{
auto_color_for_entity_path, ApplicableEntities, IdentifiedViewSystem, QueryContext,
ResolvedAnnotationInfos, SpaceViewSystemExecutionError, TypedComponentFallbackProvider,
ViewContext, ViewContextCollection, ViewQuery, VisualizableEntities, VisualizableFilterContext,
SpaceViewSystemExecutionError, TypedComponentFallbackProvider, ViewContext,
ViewContextCollection, ViewQuery, VisualizableEntities, VisualizableFilterContext,
VisualizerQueryInfo, VisualizerSystem,
};

use crate::{
contexts::SpatialSceneEntityContext,
view_kind::SpatialSpaceViewKind,
visualizers::{filter_visualizable_3d_entities, UiLabel, UiLabelTarget},
contexts::SpatialSceneEntityContext, view_kind::SpatialSpaceViewKind,
visualizers::filter_visualizable_3d_entities,
};

use super::{
entity_iterator::clamped, process_annotation_and_keypoint_slices, process_color_slice,
process_radius_slice, SpatialViewVisualizerData, SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES,
process_labels_3d, process_radius_slice, SpatialViewVisualizerData,
SIZE_BOOST_IN_POINTS_FOR_LINE_OUTLINES,
};

// ---

pub struct Arrows3DVisualizer {
/// If the number of arrows in the batch is > max_labels, don't render point labels.
pub max_labels: usize,
pub data: SpatialViewVisualizerData,
}

impl Default for Arrows3DVisualizer {
fn default() -> Self {
Self {
max_labels: 10,
data: SpatialViewVisualizerData::new(Some(SpatialSpaceViewKind::ThreeD)),
}
}
Expand All @@ -44,47 +40,6 @@ impl Default for Arrows3DVisualizer {
// NOTE: Do not put profile scopes in these methods. They are called for all entities and all
// timestamps within a time range -- it's _a lot_.
impl Arrows3DVisualizer {
fn process_labels<'a>(
entity_path: &'a EntityPath,
vectors: &'a [Vector3D],
origins: impl Iterator<Item = &'a Position3D> + 'a,
labels: &'a [Text],
colors: &'a [egui::Color32],
annotation_infos: &'a ResolvedAnnotationInfos,
world_from_obj: glam::Affine3A,
) -> impl Iterator<Item = UiLabel> + 'a {
let labels = clamped(labels, vectors.len());
let origins = origins.chain(std::iter::repeat(&Position3D::ZERO));
itertools::izip!(annotation_infos.iter(), vectors, origins, labels, colors)
.enumerate()
.filter_map(
move |(i, (annotation_info, vector, origin, label, color))| {
let label = annotation_info.label(Some(label.as_str()));
match (vector, label) {
(vector, Some(label)) => {
let midpoint =
// `0.45` rather than `0.5` to account for cap and such
(glam::Vec3::from(origin.0) + glam::Vec3::from(vector.0)) * 0.45;
let midpoint = world_from_obj.transform_point3(midpoint);

Some(UiLabel {
text: label,
color: *color,
target: UiLabelTarget::Position3D(
world_from_obj.transform_point3(midpoint),
),
labeled_instance: InstancePathHash::instance(
entity_path,
Instance::from(i as u64),
),
})
}
_ => None,
}
},
)
}

fn process_data<'a>(
&mut self,
ctx: &QueryContext<'_>,
Expand Down Expand Up @@ -117,31 +72,18 @@ impl Arrows3DVisualizer {
let colors =
process_color_slice(ctx, self, num_instances, &annotation_infos, data.colors);

if num_instances <= self.max_labels {
let origins = clamped(data.origins, num_instances);
self.data.ui_labels.extend(Self::process_labels(
entity_path,
data.vectors,
origins,
data.labels,
&colors,
&annotation_infos,
ent_context.world_from_entity,
));
}

let mut line_batch = line_builder
.batch(entity_path.to_string())
.world_from_obj(ent_context.world_from_entity)
.outline_mask_ids(ent_context.highlight.overall)
.picking_object_id(re_renderer::PickingLayerObjectId(entity_path.hash64()));

let mut bounding_box = macaw::BoundingBox::nothing();
let mut obj_space_bounding_box = macaw::BoundingBox::nothing();

let origins =
clamped(data.origins, num_instances).chain(std::iter::repeat(&Position3D::ZERO));
for (i, (vector, origin, radius, color)) in
itertools::izip!(data.vectors, origins, radii, colors).enumerate()
for (i, (vector, origin, radius, &color)) in
itertools::izip!(data.vectors, origins, radii, &colors).enumerate()
{
let vector: glam::Vec3 = vector.0.into();
let origin: glam::Vec3 = origin.0.into();
Expand All @@ -167,15 +109,43 @@ impl Arrows3DVisualizer {
segment.outline_mask_ids(*outline_mask_ids);
}

bounding_box.extend(origin);
bounding_box.extend(end);
obj_space_bounding_box.extend(origin);
obj_space_bounding_box.extend(end);
}

self.data.add_bounding_box(
entity_path.hash(),
bounding_box,
obj_space_bounding_box,
ent_context.world_from_entity,
);

if data.labels.len() == 1 || num_instances <= super::MAX_NUM_LABELS_PER_ENTITY {
// If there's many arrows but only a single label, place the single label at the middle of the visualization.
let label_positions = if data.labels.len() == 1 && num_instances > 1 {
// TODO(andreas): A smoothed over time (+ discontinuity detection) bounding box would be great.
itertools::Either::Left(std::iter::once(obj_space_bounding_box.center()))
} else {
// Take middle point of every arrow.
let origins = clamped(data.origins, num_instances)
.chain(std::iter::repeat(&Position3D::ZERO));

itertools::Either::Right(data.vectors.iter().zip(origins).map(
|(vector, origin)| {
// `0.45` rather than `0.5` to account for cap and such
(glam::Vec3::from(origin.0) + glam::Vec3::from(vector.0)) * 0.45
},
))
};

self.data.ui_labels.extend(process_labels_3d(
entity_path,
label_positions,
data.labels,
&colors,
&annotation_infos,
ent_context.world_from_entity,
));
}
}
}
}
Expand Down
Loading

0 comments on commit 9137e49

Please sign in to comment.