Skip to content

Commit

Permalink
Click recording://entity/path links in markdown (#3442)
Browse files Browse the repository at this point in the history
### What
You can now embed links to entities in markdown documents using
`recording://entity/path`, and link to components using
`recording://entity/path.Color`.

In order to support this, a new `DataPath` is introduced, with a
stricter parsing of entity paths. Previously `foo bar` was a valid path,
but now you are only allowed ASCII, numbers, underscore and dash as
names (outside of quotes).

A (uncompleted) description of the SfM example as markdown to
- Test how that feels as documentation
- Try out the ability to link directly to entities and components from
within the markdown


![image](https://github.com/rerun-io/rerun/assets/1148717/3883afe7-4c7b-42da-995d-3214fc773b52)


![image](https://github.com/rerun-io/rerun/assets/1148717/f55ddd85-297f-44ff-9dde-a0309931ade2)


### 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 [demo.rerun.io](https://demo.rerun.io/pr/3389) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/3389)
- [Docs
preview](https://rerun.io/preview/e14be611c235231fa3e91a7f106cf594bf245e51/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/e14be611c235231fa3e91a7f106cf594bf245e51/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://ref.rerun.io/dev/bench/)
- [Wasm size tracking](https://ref.rerun.io/dev/sizes/)

---------

Co-authored-by: Nikolaus West <[email protected]>
  • Loading branch information
emilk and nikolausWest authored Sep 25, 2023
1 parent d6d45a3 commit e4e1ae7
Show file tree
Hide file tree
Showing 30 changed files with 969 additions and 120 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ emath = { git = "https://github.com/emilk/egui", rev = "d949eaf" }
epaint = { git = "https://github.com/emilk/egui", rev = "d949eaf" }

# Temporary patch until next egui_commonmark release
egui_commonmark = { git = "https://github.com/lampsitter/egui_commonmark.git", rev = "a133564f26a95672e756079ac5583817e0cdaa1f" }
egui_commonmark = { git = "https://github.com/lampsitter/egui_commonmark.git", rev = "a4470c253b06a4a350e17418a7c6cbc413ade644" }

# Temporary patch until next egui_tiles release
egui_tiles = { git = "https://github.com/rerun-io/egui_tiles", rev = "c66d6cba7ddb5b236be614d1816be4561260274e" }
45 changes: 43 additions & 2 deletions crates/re_data_store/src/instance_path.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::hash::Hash;
use std::{hash::Hash, str::FromStr};

use re_log_types::{EntityPath, EntityPathHash, RowId};
use re_log_types::{DataPath, EntityPath, EntityPathHash, PathParseError, RowId};
use re_types::components::InstanceKey;

use crate::{store_db::EntityDb, VersionedInstancePath, VersionedInstancePathHash};
Expand All @@ -19,6 +19,13 @@ pub struct InstancePath {
pub instance_key: InstanceKey,
}

impl From<EntityPath> for InstancePath {
#[inline]
fn from(entity_path: EntityPath) -> Self {
Self::entity_splat(entity_path)
}
}

impl InstancePath {
/// Indicate the whole entity (all instances of it) - i.e. a splat.
///
Expand Down Expand Up @@ -77,6 +84,40 @@ impl std::fmt::Display for InstancePath {
}
}

impl FromStr for InstancePath {
type Err = PathParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let DataPath {
entity_path,
instance_key,
component_name,
} = DataPath::from_str(s)?;

if let Some(component_name) = component_name {
return Err(PathParseError::UnexpectedComponentName(component_name));
}

let instance_key = instance_key.unwrap_or(InstanceKey::SPLAT);

Ok(InstancePath {
entity_path,
instance_key,
})
}
}

#[test]
fn test_parse_instance_path() {
assert_eq!(
InstancePath::from_str("world/points[#123]"),
Ok(InstancePath {
entity_path: EntityPath::from_str("world/points").unwrap(),
instance_key: InstanceKey(123)
})
);
}

// ----------------------------------------------------------------------------

/// Hashes of the components of an [`InstancePath`].
Expand Down
5 changes: 5 additions & 0 deletions crates/re_data_store/src/store_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ impl EntityDb {
self.entity_path_from_hash.get(entity_path_hash)
}

#[inline]
pub fn knows_of_entity(&self, entity_path: &EntityPath) -> bool {
self.entity_path_from_hash.contains_key(&entity_path.hash())
}

fn register_entity_path(&mut self, entity_path: &EntityPath) {
self.entity_path_from_hash
.entry(entity_path.hash())
Expand Down
27 changes: 20 additions & 7 deletions crates/re_data_ui/src/component_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,34 @@ impl DataUi for ComponentPath {
verbosity: UiVerbosity,
query: &re_arrow_store::LatestAtQuery,
) {
let Self {
entity_path,
component_name,
} = self;

let store = &ctx.store_db.entity_db.data_store;

if let Some((_, component_data)) = re_query::get_component_with_instances(
store,
query,
self.entity_path(),
self.component_name,
) {
if let Some((_, component_data)) =
re_query::get_component_with_instances(store, query, entity_path, *component_name)
{
super::component::EntityComponentWithInstances {
entity_path: self.entity_path.clone(),
component_data,
}
.data_ui(ctx, ui, verbosity, query);
} else if let Some(entity_tree) = ctx.store_db.entity_db.tree.subtree(entity_path) {
if entity_tree.components.contains_key(component_name) {
ui.label("<unset>");
} else {
ui.label(format!(
"Entity {entity_path:?} has no component {component_name:?}"
));
}
} else {
ui.label("<unset>");
ui.label(
ctx.re_ui
.error_text(format!("Unknown entity: {entity_path:?}")),
);
}
}
}
72 changes: 64 additions & 8 deletions crates/re_data_ui/src/component_ui_registry.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use re_arrow_store::LatestAtQuery;
use re_log_types::{external::arrow2, EntityPath};
use re_query::ComponentWithInstances;
use re_types::external::arrow2::array::Utf8Array;
use re_viewer_context::{ComponentUiRegistry, UiVerbosity, ViewerContext};

use super::EntityDataUi;
Expand Down Expand Up @@ -54,33 +55,88 @@ pub fn create_component_ui_registry() -> ComponentUiRegistry {
fn fallback_component_ui(
_ctx: &mut ViewerContext<'_>,
ui: &mut egui::Ui,
_verbosity: UiVerbosity,
verbosity: UiVerbosity,
_query: &LatestAtQuery,
_entity_path: &EntityPath,
component: &ComponentWithInstances,
instance_key: &re_types::components::InstanceKey,
) {
// No special ui implementation - use a generic one:
if let Some(value) = component.lookup_arrow(instance_key) {
ui.label(format_arrow(&*value));
arrow_ui(ui, verbosity, &*value);
} else {
ui.weak("(null)");
}
}

fn format_arrow(value: &dyn arrow2::array::Array) -> String {
fn arrow_ui(ui: &mut egui::Ui, verbosity: UiVerbosity, array: &dyn arrow2::array::Array) {
use re_log_types::SizeBytes as _;

let bytes = value.total_size_bytes();
if bytes < 256 {
// Special-treat text.
// Note: we match on the raw data here, so this works for any component containing text.
if let Some(utf8) = array.as_any().downcast_ref::<Utf8Array<i32>>() {
if utf8.len() == 1 {
let string = utf8.value(0);
text_ui(string, ui, verbosity);
return;
}
}
if let Some(utf8) = array.as_any().downcast_ref::<Utf8Array<i64>>() {
if utf8.len() == 1 {
let string = utf8.value(0);
text_ui(string, ui, verbosity);
return;
}
}

let num_bytes = array.total_size_bytes();
if num_bytes < 256 {
// Print small items:
let mut string = String::new();
let display = arrow2::array::get_display(value, "null");
let display = arrow2::array::get_display(array, "null");
if display(&mut string, 0).is_ok() {
return string;
ui.label(string);
return;
}
}

// Fallback:
format!("{bytes} bytes")
ui.label(format!(
"{} of {:?}",
re_format::format_bytes(num_bytes as _),
array.data_type()
));
}

fn text_ui(string: &str, ui: &mut egui::Ui, verbosity: UiVerbosity) {
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
let color = ui.visuals().text_color();
let wrap_width = ui.available_width();
let mut layout_job =
egui::text::LayoutJob::simple(string.to_owned(), font_id, color, wrap_width);

let mut needs_scroll_area = false;

match verbosity {
UiVerbosity::Small => {
// Elide
layout_job.wrap.max_rows = 1;
layout_job.wrap.break_anywhere = true;
}
UiVerbosity::Reduced => {
layout_job.wrap.max_rows = 3;
}
UiVerbosity::All => {
let num_newlines = string.chars().filter(|&c| c == '\n').count();
needs_scroll_area = 10 < num_newlines || 300 < string.len();
}
}

if needs_scroll_area {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label(layout_job);
});
} else {
ui.label(layout_job);
}
}
39 changes: 26 additions & 13 deletions crates/re_data_ui/src/instance_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,26 @@ impl DataUi for InstancePath {
verbosity: UiVerbosity,
query: &re_arrow_store::LatestAtQuery,
) {
let Self {
entity_path,
instance_key,
} = self;

let store = &ctx.store_db.entity_db.data_store;

let Some(mut components) = store.all_components(&query.timeline, &self.entity_path) else {
ui.label(format!("No components in entity {}", self.entity_path));
let Some(mut components) = store.all_components(&query.timeline, entity_path) else {
if ctx.store_db.entity_db.knows_of_entity(entity_path) {
ui.label(format!(
"No components in entity {:?} on timeline {:?}",
entity_path,
query.timeline.name()
));
} else {
ui.label(
ctx.re_ui
.error_text(format!("Unknown entity: {entity_path:?}")),
);
}
return;
};
components.sort();
Expand All @@ -29,12 +45,9 @@ impl DataUi for InstancePath {
.num_columns(2)
.show(ui, |ui| {
for component_name in components {
let Some((_, component_data)) = get_component_with_instances(
store,
query,
&self.entity_path,
component_name,
) else {
let Some((_, component_data)) =
get_component_with_instances(store, query, entity_path, component_name)
else {
continue; // no need to show components that are unset at this point in time
};

Expand All @@ -56,12 +69,12 @@ impl DataUi for InstancePath {
item_ui::component_path_button(
ctx,
ui,
&ComponentPath::new(self.entity_path.clone(), component_name),
&ComponentPath::new(entity_path.clone(), component_name),
);

if self.instance_key.is_splat() {
if instance_key.is_splat() {
super::component::EntityComponentWithInstances {
entity_path: self.entity_path.clone(),
entity_path: entity_path.clone(),
component_data,
}
.data_ui(ctx, ui, UiVerbosity::Small, query);
Expand All @@ -71,9 +84,9 @@ impl DataUi for InstancePath {
ui,
UiVerbosity::Small,
query,
&self.entity_path,
entity_path,
&component_data,
&self.instance_key,
instance_key,
);
}

Expand Down
4 changes: 0 additions & 4 deletions crates/re_log_types/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ pub enum Index {
/// For arrays, assumed to be dense (0, 1, 2, …).
Sequence(u64),

/// X,Y pixel coordinates, from top left.
Pixel([u64; 2]),

/// Any integer, e.g. a hash or an arbitrary identifier.
Integer(i128),

Expand Down Expand Up @@ -56,7 +53,6 @@ impl std::fmt::Display for Index {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sequence(seq) => format!("#{seq}").fmt(f),
Self::Pixel([x, y]) => format!("[{x}, {y}]").fmt(f),
Self::Integer(value) => value.fmt(f),
Self::Uuid(value) => value.fmt(f),
Self::String(value) => format!("{value:?}").fmt(f), // put it in quotes
Expand Down
42 changes: 42 additions & 0 deletions crates/re_log_types/src/path/data_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use re_types::{components::InstanceKey, ComponentName};

use crate::EntityPath;

/// A general path to some data.
///
/// This always starts with an [`EntityPath`], followed
/// by an optional [`InstanceKey`], followed by an optional [`ComponentName`].
///
/// For instance:
///
/// * `points`
/// * `points.Color`
/// * `points[#42]`
/// * `points[#42].Color`
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct DataPath {
pub entity_path: EntityPath,

pub instance_key: Option<InstanceKey>,

pub component_name: Option<ComponentName>,
}

impl std::fmt::Display for DataPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.entity_path.fmt(f)?;
if let Some(instance_key) = &self.instance_key {
write!(f, "[#{}]", instance_key.0)?;
}
if let Some(component_name) = &self.component_name {
write!(f, ".{component_name:?}")?;
}
Ok(())
}
}

impl std::fmt::Debug for DataPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_string().fmt(f)
}
}
Loading

1 comment on commit e4e1ae7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Rust Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.25.

Benchmark suite Current: e4e1ae7 Previous: d6d45a3 Ratio
datastore/num_rows=1000/num_instances=1000/packed=false/latest_at/default 416 ns/iter (± 0) 323 ns/iter (± 1) 1.29
datastore/num_rows=1000/num_instances=1000/packed=false/latest_at_missing/primary/default 329 ns/iter (± 0) 240 ns/iter (± 0) 1.37
datastore/num_rows=1000/num_instances=1000/packed=false/latest_at_missing/secondaries/default 455 ns/iter (± 0) 356 ns/iter (± 0) 1.28
mono_points_arrow_batched/decode_message_bundles 7798324 ns/iter (± 10985) 5621010 ns/iter (± 33667) 1.39
mono_points_arrow_batched/decode_total 8220415 ns/iter (± 14368) 6092771 ns/iter (± 74020) 1.35

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.