From 1202bd476c46977beaad17e75e890e56cd453329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 25 Nov 2024 16:56:47 +0100 Subject: [PATCH] Implement graph components and archetypes (#7500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracking issue: #7897 This implements basic graph primitives in the Rerun data model. This is a first step towards visualizing node-link diagrams in Rerun (related issue: #6898). In addition to the changes to the data model, this PR adds two example crates, `node_link_graph` and `graph_view` to the Rust examples that show how these primitives can be used. ## Design Decisions - Nodes and edges are stored as `components` and can be batched. To have a single node per entity we can use Rerun’s [[clamping mechanism](https://rerun.io/docs/concepts/batches#component-clamping)](https://rerun.io/docs/concepts/batches#component-clamping). - `GraphNodeId` is modeled as ~`u32` to improve performance when using `petgraph`~ strings for better user experience. - A node is unique identified by combining its `GraphNodeId` and its `EntityPath`. - Labels of the nodes can be set via the `labels` component and toggled via `show_labels` - ~Hierarchical graphs can be modeled through entity paths. For edges that cross entity boundaries we can insert dummy nodes to properly render subparts of the hierarchy.~ - ~Nodes and edges need to be logged to the same entity, otherwise the selections will collide. We provider helper functions / conversions to link nodes that are stored in different entities.~ > [!WARNING] > This PR has changed considerably from its initial version: > * No linking of nodes between entities (an therefore hierarchy) yet. > * For now, Graphs are local only and therefore have to be logged to the same entity. ## Logging example ```rs rec.log( "simple", &rerun::GraphNodes::new(["a", "b", "c"]) .with_positions([(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)]) .with_labels(["A", "B", "C"]), )?; // Note: We log to the same entity here. rec.log( "simple", &rerun::GraphEdges::new([("a", "b"), ("b", "c"), ("c", "a")]).with_directed_edges(), )?; ``` ## TODOs - [x] ~Get rid of the `Default` derive for `GraphNodeId` and `GraphEdge` in the flatbuffer definitions.~ - [x] Improve ergonomics for generating graph edges during logging. - [x] Ensure that logging works from Python and C++ too. - [x] Fix remaining lints. ### 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/7500?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/7500?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)! * [x] If have noted any breaking changes to the log API in `CHANGELOG.md` and the migration guide - [PR Build Summary](https://build.rerun.io/pr/7500) - [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`. --------- Co-authored-by: Antoine Beyeler Co-authored-by: Andreas Reich --- ARCHITECTURE.md | 1 + Cargo.lock | 48 ++ Cargo.toml | 2 + .../re_types/definitions/rerun/archetypes.fbs | 2 + .../rerun/archetypes/graph_edges.fbs | 33 + .../rerun/archetypes/graph_nodes.fbs | 39 ++ .../definitions/rerun/blueprint/views.fbs | 1 + .../rerun/blueprint/views/graph.fbs | 14 + .../re_types/definitions/rerun/components.fbs | 3 + .../rerun/components/graph_edge.fbs | 12 + .../rerun/components/graph_node.fbs | 14 + .../rerun/components/graph_type.fbs | 18 + .../re_types/definitions/rerun/datatypes.fbs | 1 + .../definitions/rerun/datatypes/utf8_pair.fbs | 15 + .../re_types/src/archetypes/.gitattributes | 2 + .../re_types/src/archetypes/graph_edges.rs | 192 ++++++ .../src/archetypes/graph_edges_ext.rs | 17 + .../re_types/src/archetypes/graph_nodes.rs | 330 ++++++++++ crates/store/re_types/src/archetypes/mod.rs | 5 + .../src/blueprint/views/.gitattributes | 1 + .../src/blueprint/views/graph_view.rs | 78 +++ .../store/re_types/src/blueprint/views/mod.rs | 2 + .../re_types/src/components/.gitattributes | 3 + .../re_types/src/components/graph_edge.rs | 105 ++++ .../re_types/src/components/graph_node.rs | 105 ++++ .../re_types/src/components/graph_node_ext.rs | 16 + .../re_types/src/components/graph_type.rs | 154 +++++ crates/store/re_types/src/components/mod.rs | 7 + .../re_types/src/datatypes/.gitattributes | 1 + crates/store/re_types/src/datatypes/mod.rs | 3 + .../store/re_types/src/datatypes/utf8pair.rs | 325 ++++++++++ .../re_types/src/datatypes/utf8pair_ext.rs | 21 + crates/viewer/re_component_ui/src/lib.rs | 7 +- crates/viewer/re_space_view_graph/Cargo.toml | 38 ++ crates/viewer/re_space_view_graph/README.md | 11 + .../re_space_view_graph/src/graph/hash.rs | 43 ++ .../re_space_view_graph/src/graph/index.rs | 37 ++ .../re_space_view_graph/src/graph/mod.rs | 87 +++ .../re_space_view_graph/src/layout/mod.rs | 129 ++++ crates/viewer/re_space_view_graph/src/lib.rs | 12 + .../re_space_view_graph/src/properties.rs | 23 + .../re_space_view_graph/src/ui/draw/edge.rs | 67 +++ .../re_space_view_graph/src/ui/draw/entity.rs | 44 ++ .../re_space_view_graph/src/ui/draw/mod.rs | 7 + .../re_space_view_graph/src/ui/draw/node.rs | 83 +++ .../viewer/re_space_view_graph/src/ui/mod.rs | 10 + .../re_space_view_graph/src/ui/scene.rs | 276 +++++++++ .../re_space_view_graph/src/ui/state.rs | 185 ++++++ crates/viewer/re_space_view_graph/src/view.rs | 272 +++++++++ .../src/visualizers/edges.rs | 122 ++++ .../src/visualizers/mod.rs | 27 + .../src/visualizers/nodes.rs | 173 ++++++ .../re_ui/data/icons/spaceview_graph.png | Bin 0 -> 462 bytes crates/viewer/re_ui/src/icons.rs | 1 + crates/viewer/re_viewer/Cargo.toml | 1 + crates/viewer/re_viewer/src/app.rs | 3 +- crates/viewer/re_viewer/src/reflection/mod.rs | 76 +++ docs/content/reference/types/archetypes.md | 5 + .../reference/types/archetypes/.gitattributes | 2 + .../reference/types/archetypes/graph_edges.md | 41 ++ .../reference/types/archetypes/graph_nodes.md | 39 ++ docs/content/reference/types/components.md | 3 + .../reference/types/components/.gitattributes | 3 + .../reference/types/components/color.md | 1 + .../reference/types/components/graph_edge.md | 28 + .../reference/types/components/graph_node.md | 25 + .../reference/types/components/graph_type.md | 29 + .../reference/types/components/position2d.md | 1 + .../reference/types/components/radius.md | 1 + .../reference/types/components/show_labels.md | 1 + .../reference/types/components/text.md | 1 + docs/content/reference/types/datatypes.md | 1 + .../reference/types/datatypes/.gitattributes | 1 + .../content/reference/types/datatypes/utf8.md | 2 + .../reference/types/datatypes/utf8pair.md | 36 ++ docs/content/reference/types/views.md | 1 + .../reference/types/views/.gitattributes | 1 + .../reference/types/views/graph_view.md | 37 ++ .../all/archetypes/graph_directed.cpp | 23 + .../snippets/all/archetypes/graph_directed.py | 16 + .../snippets/all/archetypes/graph_directed.rs | 19 + .../all/archetypes/graph_undirected.cpp | 23 + .../all/archetypes/graph_undirected.py | 16 + .../all/archetypes/graph_undirected.rs | 19 + docs/snippets/all/views/graph.py | 26 + examples/manifest.toml | 2 + examples/python/README.md | 2 + examples/python/graph_binary_tree/README.md | 28 + .../graph_binary_tree/graph_binary_tree.py | 108 ++++ .../python/graph_binary_tree/pyproject.toml | 12 + examples/python/graph_lattice/README.md | 29 + .../python/graph_lattice/graph_lattice.py | 72 +++ examples/python/graph_lattice/pyproject.toml | 12 + examples/rust/graph_binary_tree/Cargo.toml | 13 + examples/rust/graph_binary_tree/README.md | 3 + examples/rust/graph_binary_tree/src/main.rs | 155 +++++ examples/rust/graph_lattice/Cargo.toml | 14 + examples/rust/graph_lattice/README.md | 3 + examples/rust/graph_lattice/src/main.rs | 64 ++ pixi.lock | 569 +++--------------- pixi.toml | 2 + rerun_cpp/src/rerun/archetypes.hpp | 2 + rerun_cpp/src/rerun/archetypes/.gitattributes | 4 + .../src/rerun/archetypes/graph_edges.cpp | 38 ++ .../src/rerun/archetypes/graph_edges.hpp | 72 +++ .../src/rerun/archetypes/graph_nodes.cpp | 58 ++ .../src/rerun/archetypes/graph_nodes.hpp | 110 ++++ rerun_cpp/src/rerun/components.hpp | 3 + rerun_cpp/src/rerun/components/.gitattributes | 4 + rerun_cpp/src/rerun/components/graph_edge.hpp | 74 +++ .../src/rerun/components/graph_edge_ext.cpp | 20 + rerun_cpp/src/rerun/components/graph_node.hpp | 78 +++ .../src/rerun/components/graph_node_ext.cpp | 19 + rerun_cpp/src/rerun/components/graph_type.cpp | 56 ++ rerun_cpp/src/rerun/components/graph_type.hpp | 56 ++ rerun_cpp/src/rerun/datatypes.hpp | 1 + rerun_cpp/src/rerun/datatypes/.gitattributes | 2 + .../rerun/datatypes/tensor_dimension_ext.cpp | 4 +- rerun_cpp/src/rerun/datatypes/utf8.hpp | 10 + rerun_cpp/src/rerun/datatypes/utf8_ext.cpp | 10 + rerun_cpp/src/rerun/datatypes/utf8pair.cpp | 81 +++ rerun_cpp/src/rerun/datatypes/utf8pair.hpp | 61 ++ .../src/rerun/datatypes/utf8pair_ext.cpp | 19 + rerun_notebook/package-lock.json | 2 +- rerun_py/docs/gen_common_index.py | 8 + rerun_py/rerun_sdk/rerun/__init__.py | 4 + .../rerun_sdk/rerun/archetypes/.gitattributes | 2 + .../rerun_sdk/rerun/archetypes/__init__.py | 4 + .../rerun_sdk/rerun/archetypes/graph_edges.py | 86 +++ .../rerun_sdk/rerun/archetypes/graph_nodes.py | 144 +++++ .../rerun_sdk/rerun/blueprint/__init__.py | 1 + .../rerun/blueprint/views/.gitattributes | 1 + .../rerun/blueprint/views/__init__.py | 2 + .../rerun/blueprint/views/graph_view.py | 125 ++++ .../rerun_sdk/rerun/components/.gitattributes | 3 + .../rerun_sdk/rerun/components/__init__.py | 11 + .../rerun_sdk/rerun/components/graph_edge.py | 32 + .../rerun_sdk/rerun/components/graph_node.py | 32 + .../rerun_sdk/rerun/components/graph_type.py | 68 +++ .../rerun_sdk/rerun/datatypes/.gitattributes | 1 + .../rerun_sdk/rerun/datatypes/__init__.py | 5 + .../rerun_sdk/rerun/datatypes/utf8pair.py | 85 +++ .../rerun_sdk/rerun/datatypes/utf8pair_ext.py | 47 ++ rerun_py/tests/unit/test_utf8pair.py | 29 + .../check_all_components_ui.py | 9 + .../release_checklist/check_graph_view.py | 73 +++ 146 files changed, 5844 insertions(+), 505 deletions(-) create mode 100644 crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs create mode 100644 crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs create mode 100644 crates/store/re_types/definitions/rerun/blueprint/views/graph.fbs create mode 100644 crates/store/re_types/definitions/rerun/components/graph_edge.fbs create mode 100644 crates/store/re_types/definitions/rerun/components/graph_node.fbs create mode 100644 crates/store/re_types/definitions/rerun/components/graph_type.fbs create mode 100644 crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs create mode 100644 crates/store/re_types/src/archetypes/graph_edges.rs create mode 100644 crates/store/re_types/src/archetypes/graph_edges_ext.rs create mode 100644 crates/store/re_types/src/archetypes/graph_nodes.rs create mode 100644 crates/store/re_types/src/blueprint/views/graph_view.rs create mode 100644 crates/store/re_types/src/components/graph_edge.rs create mode 100644 crates/store/re_types/src/components/graph_node.rs create mode 100644 crates/store/re_types/src/components/graph_node_ext.rs create mode 100644 crates/store/re_types/src/components/graph_type.rs create mode 100644 crates/store/re_types/src/datatypes/utf8pair.rs create mode 100644 crates/store/re_types/src/datatypes/utf8pair_ext.rs create mode 100644 crates/viewer/re_space_view_graph/Cargo.toml create mode 100644 crates/viewer/re_space_view_graph/README.md create mode 100644 crates/viewer/re_space_view_graph/src/graph/hash.rs create mode 100644 crates/viewer/re_space_view_graph/src/graph/index.rs create mode 100644 crates/viewer/re_space_view_graph/src/graph/mod.rs create mode 100644 crates/viewer/re_space_view_graph/src/layout/mod.rs create mode 100644 crates/viewer/re_space_view_graph/src/lib.rs create mode 100644 crates/viewer/re_space_view_graph/src/properties.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/draw/edge.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/draw/entity.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/draw/mod.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/draw/node.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/mod.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/scene.rs create mode 100644 crates/viewer/re_space_view_graph/src/ui/state.rs create mode 100644 crates/viewer/re_space_view_graph/src/view.rs create mode 100644 crates/viewer/re_space_view_graph/src/visualizers/edges.rs create mode 100644 crates/viewer/re_space_view_graph/src/visualizers/mod.rs create mode 100644 crates/viewer/re_space_view_graph/src/visualizers/nodes.rs create mode 100644 crates/viewer/re_ui/data/icons/spaceview_graph.png create mode 100644 docs/content/reference/types/archetypes/graph_edges.md create mode 100644 docs/content/reference/types/archetypes/graph_nodes.md create mode 100644 docs/content/reference/types/components/graph_edge.md create mode 100644 docs/content/reference/types/components/graph_node.md create mode 100644 docs/content/reference/types/components/graph_type.md create mode 100644 docs/content/reference/types/datatypes/utf8pair.md create mode 100644 docs/content/reference/types/views/graph_view.md create mode 100644 docs/snippets/all/archetypes/graph_directed.cpp create mode 100644 docs/snippets/all/archetypes/graph_directed.py create mode 100644 docs/snippets/all/archetypes/graph_directed.rs create mode 100644 docs/snippets/all/archetypes/graph_undirected.cpp create mode 100644 docs/snippets/all/archetypes/graph_undirected.py create mode 100644 docs/snippets/all/archetypes/graph_undirected.rs create mode 100644 docs/snippets/all/views/graph.py create mode 100644 examples/python/graph_binary_tree/README.md create mode 100644 examples/python/graph_binary_tree/graph_binary_tree.py create mode 100644 examples/python/graph_binary_tree/pyproject.toml create mode 100644 examples/python/graph_lattice/README.md create mode 100644 examples/python/graph_lattice/graph_lattice.py create mode 100644 examples/python/graph_lattice/pyproject.toml create mode 100644 examples/rust/graph_binary_tree/Cargo.toml create mode 100644 examples/rust/graph_binary_tree/README.md create mode 100644 examples/rust/graph_binary_tree/src/main.rs create mode 100644 examples/rust/graph_lattice/Cargo.toml create mode 100644 examples/rust/graph_lattice/README.md create mode 100644 examples/rust/graph_lattice/src/main.rs create mode 100644 rerun_cpp/src/rerun/archetypes/graph_edges.cpp create mode 100644 rerun_cpp/src/rerun/archetypes/graph_edges.hpp create mode 100644 rerun_cpp/src/rerun/archetypes/graph_nodes.cpp create mode 100644 rerun_cpp/src/rerun/archetypes/graph_nodes.hpp create mode 100644 rerun_cpp/src/rerun/components/graph_edge.hpp create mode 100644 rerun_cpp/src/rerun/components/graph_edge_ext.cpp create mode 100644 rerun_cpp/src/rerun/components/graph_node.hpp create mode 100644 rerun_cpp/src/rerun/components/graph_node_ext.cpp create mode 100644 rerun_cpp/src/rerun/components/graph_type.cpp create mode 100644 rerun_cpp/src/rerun/components/graph_type.hpp create mode 100644 rerun_cpp/src/rerun/datatypes/utf8pair.cpp create mode 100644 rerun_cpp/src/rerun/datatypes/utf8pair.hpp create mode 100644 rerun_cpp/src/rerun/datatypes/utf8pair_ext.cpp create mode 100644 rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py create mode 100644 rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py create mode 100644 rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py create mode 100644 rerun_py/rerun_sdk/rerun/components/graph_edge.py create mode 100644 rerun_py/rerun_sdk/rerun/components/graph_node.py create mode 100644 rerun_py/rerun_sdk/rerun/components/graph_type.py create mode 100644 rerun_py/rerun_sdk/rerun/datatypes/utf8pair.py create mode 100644 rerun_py/rerun_sdk/rerun/datatypes/utf8pair_ext.py create mode 100644 rerun_py/tests/unit/test_utf8pair.py create mode 100644 tests/python/release_checklist/check_graph_view.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9e38ba1cd9a1..9c7f401948e2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -134,6 +134,7 @@ Update instructions: | re_space_view | Types & utilities for defining Space View classes and communicating with the Viewport. | | re_space_view_bar_chart | A Space View that shows a single bar chart. | | re_space_view_dataframe | A Space View that shows the data contained in entities in a table. | +| re_space_view_graph | A Space View that shows a graph (node-link diagram). | | re_space_view_map | A Space View that shows geospatial data on a map. | | re_space_view_spatial | Space Views that show entities in a 2D or 3D spatial relationship. | | re_space_view_tensor | A Space View dedicated to visualizing tensors with arbitrary dimensionality. | diff --git a/Cargo.lock b/Cargo.lock index 8452a4a7f59b..4785e53ca5c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2376,6 +2376,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fjadra" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0416b27f53bba6c9bd564260f27d4784c5f430926eb16a519356be7d66bbc" + [[package]] name = "flatbuffers" version = "23.5.26" @@ -2754,6 +2760,25 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "graph_binary_tree" +version = "0.21.0-alpha.1+dev" +dependencies = [ + "anyhow", + "clap", + "rerun", +] + +[[package]] +name = "graph_lattice" +version = "0.21.0-alpha.1+dev" +dependencies = [ + "anyhow", + "clap", + "itertools 0.13.0", + "rerun", +] + [[package]] name = "h2" version = "0.3.26" @@ -6052,6 +6077,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "re_space_view_graph" +version = "0.21.0-alpha.1+dev" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "fjadra", + "nohash-hasher", + "re_chunk", + "re_format", + "re_log_types", + "re_query", + "re_renderer", + "re_space_view", + "re_tracing", + "re_types", + "re_ui", + "re_viewer_context", + "re_viewport_blueprint", +] + [[package]] name = "re_space_view_map" version = "0.21.0-alpha.1+dev" @@ -6462,6 +6509,7 @@ dependencies = [ "re_smart_channel", "re_space_view_bar_chart", "re_space_view_dataframe", + "re_space_view_graph", "re_space_view_map", "re_space_view_spatial", "re_space_view_tensor", diff --git a/Cargo.toml b/Cargo.toml index ab4a9697a76f..cde6b63e1d33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ re_space_view = { path = "crates/viewer/re_space_view", version = "=0.21.0-alpha re_space_view_bar_chart = { path = "crates/viewer/re_space_view_bar_chart", version = "=0.21.0-alpha.1", default-features = false } re_space_view_spatial = { path = "crates/viewer/re_space_view_spatial", version = "=0.21.0-alpha.1", default-features = false } re_space_view_dataframe = { path = "crates/viewer/re_space_view_dataframe", version = "=0.21.0-alpha.1", default-features = false } +re_space_view_graph = { path = "crates/viewer/re_space_view_graph", version = "=0.21.0-alpha.1", default-features = false } re_space_view_map = { path = "crates/viewer/re_space_view_map", version = "=0.21.0-alpha.1", default-features = false } re_space_view_tensor = { path = "crates/viewer/re_space_view_tensor", version = "=0.21.0-alpha.1", default-features = false } re_space_view_text_document = { path = "crates/viewer/re_space_view_text_document", version = "=0.21.0-alpha.1", default-features = false } @@ -186,6 +187,7 @@ enumset = "1.0.12" env_logger = { version = "0.10", default-features = false } ffmpeg-sidecar = { version = "2.0.2", default-features = false } fixed = { version = "1.28", default-features = false } +fjadra = "0.1" flatbuffers = "23.0" futures-channel = "0.3" futures-util = { version = "0.3", default-features = false } diff --git a/crates/store/re_types/definitions/rerun/archetypes.fbs b/crates/store/re_types/definitions/rerun/archetypes.fbs index 0dfb4f20c146..e2e78c427d09 100644 --- a/crates/store/re_types/definitions/rerun/archetypes.fbs +++ b/crates/store/re_types/definitions/rerun/archetypes.fbs @@ -16,6 +16,8 @@ include "./archetypes/ellipsoids3d.fbs"; include "./archetypes/encoded_image.fbs"; include "./archetypes/geo_line_strings.fbs"; include "./archetypes/geo_points.fbs"; +include "./archetypes/graph_edges.fbs"; +include "./archetypes/graph_nodes.fbs"; include "./archetypes/image.fbs"; include "./archetypes/instance_poses3d.fbs"; include "./archetypes/line_strips2d.fbs"; diff --git a/crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs b/crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs new file mode 100644 index 000000000000..0d18836848b4 --- /dev/null +++ b/crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs @@ -0,0 +1,33 @@ +namespace rerun.archetypes; + +// --- + +// TODO(ab): Add images to snippets. + +/// A list of edges in a graph. +/// +/// By default, edges are undirected. +/// +/// \example archetypes/graph_undirected !api title="Simple undirected graph" image="" +/// \example archetypes/graph_directed !api title="Simple directed graph" image="" +table GraphEdges ( + "attr.docs.category": "Graph", + "attr.docs.unreleased", + "attr.docs.view_types": "GraphView", + "attr.rust.derive": "PartialEq, Eq", + "attr.rerun.experimental" +) { + // --- Required --- + + /// A list of node tuples. + edges: [rerun.components.GraphEdge] ("attr.rerun.component_required", order: 1000); + + + // --- Recommended --- + + /// Specifies if the graph is directed or undirected. + /// + /// If no `GraphType` is provided, the graph is assumed to be undirected. + graph_type: rerun.components.GraphType ("attr.rerun.component_recommended", nullable, order: 2000); + +} diff --git a/crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs b/crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs new file mode 100644 index 000000000000..bf31f6fe5efa --- /dev/null +++ b/crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs @@ -0,0 +1,39 @@ +namespace rerun.archetypes; + +// --- + +// TODO(ab): Add images to snippets. + +/// A list of nodes in a graph with optional labels, colors, etc. +/// +/// \example archetypes/graph_undirected !api title="Simple undirected graph" image="" +/// \example archetypes/graph_directed !api title="Simple directed graph" image="" +table GraphNodes ( + "attr.docs.category": "Graph", + "attr.docs.unreleased", + "attr.docs.view_types": "GraphView", + "attr.rust.derive": "PartialEq", + "attr.rerun.experimental" +) { + // --- Required --- + + /// A list of node IDs. + node_ids: [rerun.components.GraphNode] ("attr.rerun.component_required", order: 1000); + + // --- Optional --- + + /// Optional center positions of the nodes. + positions: [rerun.components.Position2D] ("attr.rerun.component_optional", nullable, order: 3000); + + /// Optional colors for the boxes. + colors: [rerun.components.Color] ("attr.rerun.component_optional", nullable, order: 3100); + + /// Optional text labels for the node. + labels: [rerun.components.Text] ("attr.rerun.component_optional", nullable, order: 3200); + + /// Optional choice of whether the text labels should be shown by default. + show_labels: rerun.components.ShowLabels ("attr.rerun.component_optional", nullable, order: 3250); + + /// Optional radii for nodes. + radii: [rerun.components.Radius] ("attr.rerun.component_optional", nullable, order: 3300); +} diff --git a/crates/store/re_types/definitions/rerun/blueprint/views.fbs b/crates/store/re_types/definitions/rerun/blueprint/views.fbs index 8558245c8779..a1e81ce6ca08 100644 --- a/crates/store/re_types/definitions/rerun/blueprint/views.fbs +++ b/crates/store/re_types/definitions/rerun/blueprint/views.fbs @@ -2,6 +2,7 @@ include "./views/bar_chart.fbs"; include "./views/dataframe.fbs"; +include "./views/graph.fbs"; include "./views/map.fbs"; include "./views/spatial2d.fbs"; include "./views/spatial3d.fbs"; diff --git a/crates/store/re_types/definitions/rerun/blueprint/views/graph.fbs b/crates/store/re_types/definitions/rerun/blueprint/views/graph.fbs new file mode 100644 index 000000000000..2f808bb8df2a --- /dev/null +++ b/crates/store/re_types/definitions/rerun/blueprint/views/graph.fbs @@ -0,0 +1,14 @@ +namespace rerun.blueprint.views; + +/// A graph view to display time-variying, directed or undirected graph visualization. +/// +/// \example views/graph title="Use a blueprint to create a graph view." image="https://static.rerun.io/graph_lattice/f9169da9c3f35b7260c9d74cd5be5fe710aec6a8/1200w.png" +table GraphView ( + "attr.rerun.view_identifier": "Graph", + "attr.docs.unreleased" +) { + /// Everything within these bounds is guaranteed to be visible. + /// + /// Somethings outside of these bounds may also be visible due to letterboxing. + visual_bounds: rerun.blueprint.archetypes.VisualBounds2D (order: 1000); +} diff --git a/crates/store/re_types/definitions/rerun/components.fbs b/crates/store/re_types/definitions/rerun/components.fbs index b1a32fd2133f..ca625053e29f 100644 --- a/crates/store/re_types/definitions/rerun/components.fbs +++ b/crates/store/re_types/definitions/rerun/components.fbs @@ -17,6 +17,9 @@ include "./components/fill_mode.fbs"; include "./components/fill_ratio.fbs"; include "./components/gamma_correction.fbs"; include "./components/geo_line_string.fbs"; +include "./components/graph_edge.fbs"; +include "./components/graph_node.fbs"; +include "./components/graph_type.fbs"; include "./components/half_size2d.fbs"; include "./components/half_size3d.fbs"; include "./components/image_buffer.fbs"; diff --git a/crates/store/re_types/definitions/rerun/components/graph_edge.fbs b/crates/store/re_types/definitions/rerun/components/graph_edge.fbs new file mode 100644 index 000000000000..1f97a2e6c506 --- /dev/null +++ b/crates/store/re_types/definitions/rerun/components/graph_edge.fbs @@ -0,0 +1,12 @@ +namespace rerun.components; + +// --- + +/// An edge in a graph connecting two nodes. +table GraphEdge ( + "attr.docs.unreleased", + "attr.rust.derive": "Default, PartialEq, Eq, PartialOrd, Ord", + "attr.rust.repr": "transparent" +) { + edge: rerun.datatypes.Utf8Pair (order: 100); +} diff --git a/crates/store/re_types/definitions/rerun/components/graph_node.fbs b/crates/store/re_types/definitions/rerun/components/graph_node.fbs new file mode 100644 index 000000000000..7867665a380a --- /dev/null +++ b/crates/store/re_types/definitions/rerun/components/graph_node.fbs @@ -0,0 +1,14 @@ +namespace rerun.components; + +// --- + +/// A string-based ID representing a node in a graph. +table GraphNode ( + "attr.docs.unreleased", + "attr.python.aliases": "str", + "attr.python.array_aliases": "str, Sequence[str]", + "attr.rust.derive": "Default, PartialEq, Eq, PartialOrd, Ord, Hash", + "attr.rust.repr": "transparent" +) { + id: rerun.datatypes.Utf8 (order: 100); +} diff --git a/crates/store/re_types/definitions/rerun/components/graph_type.fbs b/crates/store/re_types/definitions/rerun/components/graph_type.fbs new file mode 100644 index 000000000000..283e3a2b93b4 --- /dev/null +++ b/crates/store/re_types/definitions/rerun/components/graph_type.fbs @@ -0,0 +1,18 @@ +namespace rerun.components; + +// -- + +/// Specifies if a graph has directed or undirected edges. +enum GraphType: ubyte ( + "attr.docs.unreleased", + "attr.rust.derive": "Default, PartialEq, Eq" +) { + /// Invalid value. Won't show up in generated types. + Invalid = 0, + + /// The graph has undirected edges. + Undirected (default), + + /// The graph has directed edges. + Directed, +} diff --git a/crates/store/re_types/definitions/rerun/datatypes.fbs b/crates/store/re_types/definitions/rerun/datatypes.fbs index 03fcafbc18da..c56691a867b2 100644 --- a/crates/store/re_types/definitions/rerun/datatypes.fbs +++ b/crates/store/re_types/definitions/rerun/datatypes.fbs @@ -33,6 +33,7 @@ include "./datatypes/uint16.fbs"; include "./datatypes/uint32.fbs"; include "./datatypes/uint64.fbs"; include "./datatypes/utf8.fbs"; +include "./datatypes/utf8_pair.fbs"; include "./datatypes/uuid.fbs"; include "./datatypes/uvec2d.fbs"; include "./datatypes/uvec3d.fbs"; diff --git a/crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs b/crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs new file mode 100644 index 000000000000..9acbb85a5052 --- /dev/null +++ b/crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs @@ -0,0 +1,15 @@ +namespace rerun.datatypes; + +/// Stores a tuple of UTF-8 strings. +table Utf8Pair ( + "attr.docs.unreleased", + "attr.python.aliases": "Tuple[datatypes.Utf8Like, datatypes.Utf8Like]", + "attr.python.array_aliases": "npt.NDArray[np.str_]", + "attr.rust.derive": "Default, PartialEq, Eq, PartialOrd, Ord" +) { + /// The first string. + first: rerun.datatypes.Utf8 (order: 100); + + /// The second string. + second: rerun.datatypes.Utf8 (order: 200); +} diff --git a/crates/store/re_types/src/archetypes/.gitattributes b/crates/store/re_types/src/archetypes/.gitattributes index 8496794357eb..47e1b0870bce 100644 --- a/crates/store/re_types/src/archetypes/.gitattributes +++ b/crates/store/re_types/src/archetypes/.gitattributes @@ -16,6 +16,8 @@ ellipsoids3d.rs linguist-generated=true encoded_image.rs linguist-generated=true geo_line_strings.rs linguist-generated=true geo_points.rs linguist-generated=true +graph_edges.rs linguist-generated=true +graph_nodes.rs linguist-generated=true image.rs linguist-generated=true instance_poses3d.rs linguist-generated=true line_strips2d.rs linguist-generated=true diff --git a/crates/store/re_types/src/archetypes/graph_edges.rs b/crates/store/re_types/src/archetypes/graph_edges.rs new file mode 100644 index 000000000000..73b5ede6aa78 --- /dev/null +++ b/crates/store/re_types/src/archetypes/graph_edges.rs @@ -0,0 +1,192 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: A list of edges in a graph. +/// +/// By default, edges are undirected. +/// +/// ⚠️ **This type is experimental and may be removed in future versions** +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GraphEdges { + /// A list of node tuples. + pub edges: Vec, + + /// Specifies if the graph is directed or undirected. + /// + /// If no `GraphType` is provided, the graph is assumed to be undirected. + pub graph_type: Option, +} + +impl ::re_types_core::SizeBytes for GraphEdges { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.edges.heap_size_bytes() + self.graph_type.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + >::is_pod() + && >::is_pod() + } +} + +static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.components.GraphEdge".into()]); + +static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 2usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.components.GraphType".into(), + "rerun.components.GraphEdgesIndicator".into(), + ] + }); + +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 0usize]> = + once_cell::sync::Lazy::new(|| []); + +static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 3usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.components.GraphEdge".into(), + "rerun.components.GraphType".into(), + "rerun.components.GraphEdgesIndicator".into(), + ] + }); + +impl GraphEdges { + /// The total number of components in the archetype: 1 required, 2 recommended, 0 optional + pub const NUM_COMPONENTS: usize = 3usize; +} + +/// Indicator component for the [`GraphEdges`] [`::re_types_core::Archetype`] +pub type GraphEdgesIndicator = ::re_types_core::GenericIndicatorComponent; + +impl ::re_types_core::Archetype for GraphEdges { + type Indicator = GraphEdgesIndicator; + + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + "rerun.archetypes.GraphEdges".into() + } + + #[inline] + fn display_name() -> &'static str { + "Graph edges" + } + + #[inline] + fn indicator() -> MaybeOwnedComponentBatch<'static> { + static INDICATOR: GraphEdgesIndicator = GraphEdgesIndicator::DEFAULT; + MaybeOwnedComponentBatch::Ref(&INDICATOR) + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow2_components( + arrow_data: impl IntoIterator)>, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data + .into_iter() + .map(|(name, array)| (name.full_name(), array)) + .collect(); + let edges = { + let array = arrays_by_name + .get("rerun.components.GraphEdge") + .ok_or_else(DeserializationError::missing_data) + .with_context("rerun.archetypes.GraphEdges#edges")?; + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphEdges#edges")? + .into_iter() + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .collect::>>() + .with_context("rerun.archetypes.GraphEdges#edges")? + }; + let graph_type = if let Some(array) = arrays_by_name.get("rerun.components.GraphType") { + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphEdges#graph_type")? + .into_iter() + .next() + .flatten() + } else { + None + }; + Ok(Self { edges, graph_type }) + } +} + +impl ::re_types_core::AsComponents for GraphEdges { + fn as_component_batches(&self) -> Vec> { + re_tracing::profile_function!(); + use ::re_types_core::Archetype as _; + [ + Some(Self::indicator()), + Some((&self.edges as &dyn ComponentBatch).into()), + self.graph_type + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), + ] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for GraphEdges {} + +impl GraphEdges { + /// Create a new `GraphEdges`. + #[inline] + pub fn new(edges: impl IntoIterator>) -> Self { + Self { + edges: edges.into_iter().map(Into::into).collect(), + graph_type: None, + } + } + + /// Specifies if the graph is directed or undirected. + /// + /// If no `GraphType` is provided, the graph is assumed to be undirected. + #[inline] + pub fn with_graph_type(mut self, graph_type: impl Into) -> Self { + self.graph_type = Some(graph_type.into()); + self + } +} diff --git a/crates/store/re_types/src/archetypes/graph_edges_ext.rs b/crates/store/re_types/src/archetypes/graph_edges_ext.rs new file mode 100644 index 000000000000..9e97e0797882 --- /dev/null +++ b/crates/store/re_types/src/archetypes/graph_edges_ext.rs @@ -0,0 +1,17 @@ +use super::GraphEdges; + +impl GraphEdges { + /// Creates a graph with undirected edges. + #[inline(always)] + pub fn with_undirected_edges(mut self) -> Self { + self.graph_type = Some(crate::components::GraphType::Undirected); + self + } + + /// Creates a graph with directed edges. + #[inline(always)] + pub fn with_directed_edges(mut self) -> Self { + self.graph_type = Some(crate::components::GraphType::Directed); + self + } +} diff --git a/crates/store/re_types/src/archetypes/graph_nodes.rs b/crates/store/re_types/src/archetypes/graph_nodes.rs new file mode 100644 index 000000000000..96e10bc1418d --- /dev/null +++ b/crates/store/re_types/src/archetypes/graph_nodes.rs @@ -0,0 +1,330 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Archetype**: A list of nodes in a graph with optional labels, colors, etc. +/// +/// ⚠️ **This type is experimental and may be removed in future versions** +#[derive(Clone, Debug, PartialEq)] +pub struct GraphNodes { + /// A list of node IDs. + pub node_ids: Vec, + + /// Optional center positions of the nodes. + pub positions: Option>, + + /// Optional colors for the boxes. + pub colors: Option>, + + /// Optional text labels for the node. + pub labels: Option>, + + /// Optional choice of whether the text labels should be shown by default. + pub show_labels: Option, + + /// Optional radii for nodes. + pub radii: Option>, +} + +impl ::re_types_core::SizeBytes for GraphNodes { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.node_ids.heap_size_bytes() + + self.positions.heap_size_bytes() + + self.colors.heap_size_bytes() + + self.labels.heap_size_bytes() + + self.show_labels.heap_size_bytes() + + self.radii.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + >::is_pod() + && >>::is_pod() + && >>::is_pod() + && >>::is_pod() + && >::is_pod() + && >>::is_pod() + } +} + +static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.components.GraphNode".into()]); + +static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.components.GraphNodesIndicator".into()]); + +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 5usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.components.Position2D".into(), + "rerun.components.Color".into(), + "rerun.components.Text".into(), + "rerun.components.ShowLabels".into(), + "rerun.components.Radius".into(), + ] + }); + +static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 7usize]> = + once_cell::sync::Lazy::new(|| { + [ + "rerun.components.GraphNode".into(), + "rerun.components.GraphNodesIndicator".into(), + "rerun.components.Position2D".into(), + "rerun.components.Color".into(), + "rerun.components.Text".into(), + "rerun.components.ShowLabels".into(), + "rerun.components.Radius".into(), + ] + }); + +impl GraphNodes { + /// The total number of components in the archetype: 1 required, 1 recommended, 5 optional + pub const NUM_COMPONENTS: usize = 7usize; +} + +/// Indicator component for the [`GraphNodes`] [`::re_types_core::Archetype`] +pub type GraphNodesIndicator = ::re_types_core::GenericIndicatorComponent; + +impl ::re_types_core::Archetype for GraphNodes { + type Indicator = GraphNodesIndicator; + + #[inline] + fn name() -> ::re_types_core::ArchetypeName { + "rerun.archetypes.GraphNodes".into() + } + + #[inline] + fn display_name() -> &'static str { + "Graph nodes" + } + + #[inline] + fn indicator() -> MaybeOwnedComponentBatch<'static> { + static INDICATOR: GraphNodesIndicator = GraphNodesIndicator::DEFAULT; + MaybeOwnedComponentBatch::Ref(&INDICATOR) + } + + #[inline] + fn required_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + REQUIRED_COMPONENTS.as_slice().into() + } + + #[inline] + fn recommended_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + RECOMMENDED_COMPONENTS.as_slice().into() + } + + #[inline] + fn optional_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + OPTIONAL_COMPONENTS.as_slice().into() + } + + #[inline] + fn all_components() -> ::std::borrow::Cow<'static, [ComponentName]> { + ALL_COMPONENTS.as_slice().into() + } + + #[inline] + fn from_arrow2_components( + arrow_data: impl IntoIterator)>, + ) -> DeserializationResult { + re_tracing::profile_function!(); + use ::re_types_core::{Loggable as _, ResultExt as _}; + let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data + .into_iter() + .map(|(name, array)| (name.full_name(), array)) + .collect(); + let node_ids = { + let array = arrays_by_name + .get("rerun.components.GraphNode") + .ok_or_else(DeserializationError::missing_data) + .with_context("rerun.archetypes.GraphNodes#node_ids")?; + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphNodes#node_ids")? + .into_iter() + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .collect::>>() + .with_context("rerun.archetypes.GraphNodes#node_ids")? + }; + let positions = if let Some(array) = arrays_by_name.get("rerun.components.Position2D") { + Some({ + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphNodes#positions")? + .into_iter() + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .collect::>>() + .with_context("rerun.archetypes.GraphNodes#positions")? + }) + } else { + None + }; + let colors = if let Some(array) = arrays_by_name.get("rerun.components.Color") { + Some({ + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphNodes#colors")? + .into_iter() + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .collect::>>() + .with_context("rerun.archetypes.GraphNodes#colors")? + }) + } else { + None + }; + let labels = if let Some(array) = arrays_by_name.get("rerun.components.Text") { + Some({ + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphNodes#labels")? + .into_iter() + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .collect::>>() + .with_context("rerun.archetypes.GraphNodes#labels")? + }) + } else { + None + }; + let show_labels = if let Some(array) = arrays_by_name.get("rerun.components.ShowLabels") { + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphNodes#show_labels")? + .into_iter() + .next() + .flatten() + } else { + None + }; + let radii = if let Some(array) = arrays_by_name.get("rerun.components.Radius") { + Some({ + ::from_arrow2_opt(&**array) + .with_context("rerun.archetypes.GraphNodes#radii")? + .into_iter() + .map(|v| v.ok_or_else(DeserializationError::missing_data)) + .collect::>>() + .with_context("rerun.archetypes.GraphNodes#radii")? + }) + } else { + None + }; + Ok(Self { + node_ids, + positions, + colors, + labels, + show_labels, + radii, + }) + } +} + +impl ::re_types_core::AsComponents for GraphNodes { + fn as_component_batches(&self) -> Vec> { + re_tracing::profile_function!(); + use ::re_types_core::Archetype as _; + [ + Some(Self::indicator()), + Some((&self.node_ids as &dyn ComponentBatch).into()), + self.positions + .as_ref() + .map(|comp_batch| (comp_batch as &dyn ComponentBatch).into()), + self.colors + .as_ref() + .map(|comp_batch| (comp_batch as &dyn ComponentBatch).into()), + self.labels + .as_ref() + .map(|comp_batch| (comp_batch as &dyn ComponentBatch).into()), + self.show_labels + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), + self.radii + .as_ref() + .map(|comp_batch| (comp_batch as &dyn ComponentBatch).into()), + ] + .into_iter() + .flatten() + .collect() + } +} + +impl ::re_types_core::ArchetypeReflectionMarker for GraphNodes {} + +impl GraphNodes { + /// Create a new `GraphNodes`. + #[inline] + pub fn new( + node_ids: impl IntoIterator>, + ) -> Self { + Self { + node_ids: node_ids.into_iter().map(Into::into).collect(), + positions: None, + colors: None, + labels: None, + show_labels: None, + radii: None, + } + } + + /// Optional center positions of the nodes. + #[inline] + pub fn with_positions( + mut self, + positions: impl IntoIterator>, + ) -> Self { + self.positions = Some(positions.into_iter().map(Into::into).collect()); + self + } + + /// Optional colors for the boxes. + #[inline] + pub fn with_colors( + mut self, + colors: impl IntoIterator>, + ) -> Self { + self.colors = Some(colors.into_iter().map(Into::into).collect()); + self + } + + /// Optional text labels for the node. + #[inline] + pub fn with_labels( + mut self, + labels: impl IntoIterator>, + ) -> Self { + self.labels = Some(labels.into_iter().map(Into::into).collect()); + self + } + + /// Optional choice of whether the text labels should be shown by default. + #[inline] + pub fn with_show_labels( + mut self, + show_labels: impl Into, + ) -> Self { + self.show_labels = Some(show_labels.into()); + self + } + + /// Optional radii for nodes. + #[inline] + pub fn with_radii( + mut self, + radii: impl IntoIterator>, + ) -> Self { + self.radii = Some(radii.into_iter().map(Into::into).collect()); + self + } +} diff --git a/crates/store/re_types/src/archetypes/mod.rs b/crates/store/re_types/src/archetypes/mod.rs index 8b2c7bc946a1..3bac64ce396e 100644 --- a/crates/store/re_types/src/archetypes/mod.rs +++ b/crates/store/re_types/src/archetypes/mod.rs @@ -27,6 +27,9 @@ mod geo_line_strings; mod geo_line_strings_ext; mod geo_points; mod geo_points_ext; +mod graph_edges; +mod graph_edges_ext; +mod graph_nodes; mod image; mod image_ext; mod instance_poses3d; @@ -70,6 +73,8 @@ pub use self::ellipsoids3d::Ellipsoids3D; pub use self::encoded_image::EncodedImage; pub use self::geo_line_strings::GeoLineStrings; pub use self::geo_points::GeoPoints; +pub use self::graph_edges::GraphEdges; +pub use self::graph_nodes::GraphNodes; pub use self::image::Image; pub use self::instance_poses3d::InstancePoses3D; pub use self::line_strips2d::LineStrips2D; diff --git a/crates/store/re_types/src/blueprint/views/.gitattributes b/crates/store/re_types/src/blueprint/views/.gitattributes index 53616b1e6e83..c49cd98b51ee 100644 --- a/crates/store/re_types/src/blueprint/views/.gitattributes +++ b/crates/store/re_types/src/blueprint/views/.gitattributes @@ -3,6 +3,7 @@ .gitattributes linguist-generated=true bar_chart_view.rs linguist-generated=true dataframe_view.rs linguist-generated=true +graph_view.rs linguist-generated=true map_view.rs linguist-generated=true mod.rs linguist-generated=true spatial2d_view.rs linguist-generated=true diff --git a/crates/store/re_types/src/blueprint/views/graph_view.rs b/crates/store/re_types/src/blueprint/views/graph_view.rs new file mode 100644 index 000000000000..505d5efd40d1 --- /dev/null +++ b/crates/store/re_types/src/blueprint/views/graph_view.rs @@ -0,0 +1,78 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/blueprint/views/graph.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **View**: A graph view to display time-variying, directed or undirected graph visualization. +#[derive(Clone, Debug)] +pub struct GraphView { + /// Everything within these bounds is guaranteed to be visible. + /// + /// Somethings outside of these bounds may also be visible due to letterboxing. + pub visual_bounds: crate::blueprint::archetypes::VisualBounds2D, +} + +impl ::re_types_core::SizeBytes for GraphView { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.visual_bounds.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} + +impl> From for GraphView { + fn from(v: T) -> Self { + Self { + visual_bounds: v.into(), + } + } +} + +impl std::borrow::Borrow for GraphView { + #[inline] + fn borrow(&self) -> &crate::blueprint::archetypes::VisualBounds2D { + &self.visual_bounds + } +} + +impl std::ops::Deref for GraphView { + type Target = crate::blueprint::archetypes::VisualBounds2D; + + #[inline] + fn deref(&self) -> &crate::blueprint::archetypes::VisualBounds2D { + &self.visual_bounds + } +} + +impl std::ops::DerefMut for GraphView { + #[inline] + fn deref_mut(&mut self) -> &mut crate::blueprint::archetypes::VisualBounds2D { + &mut self.visual_bounds + } +} + +impl ::re_types_core::View for GraphView { + #[inline] + fn identifier() -> ::re_types_core::SpaceViewClassIdentifier { + "Graph".into() + } +} diff --git a/crates/store/re_types/src/blueprint/views/mod.rs b/crates/store/re_types/src/blueprint/views/mod.rs index c8355f34eefb..11802f945c62 100644 --- a/crates/store/re_types/src/blueprint/views/mod.rs +++ b/crates/store/re_types/src/blueprint/views/mod.rs @@ -2,6 +2,7 @@ mod bar_chart_view; mod dataframe_view; +mod graph_view; mod map_view; mod spatial2d_view; mod spatial3d_view; @@ -12,6 +13,7 @@ mod time_series_view; pub use self::bar_chart_view::BarChartView; pub use self::dataframe_view::DataframeView; +pub use self::graph_view::GraphView; pub use self::map_view::MapView; pub use self::spatial2d_view::Spatial2DView; pub use self::spatial3d_view::Spatial3DView; diff --git a/crates/store/re_types/src/components/.gitattributes b/crates/store/re_types/src/components/.gitattributes index def154d84908..0a17e84f4293 100644 --- a/crates/store/re_types/src/components/.gitattributes +++ b/crates/store/re_types/src/components/.gitattributes @@ -17,6 +17,9 @@ fill_mode.rs linguist-generated=true fill_ratio.rs linguist-generated=true gamma_correction.rs linguist-generated=true geo_line_string.rs linguist-generated=true +graph_edge.rs linguist-generated=true +graph_node.rs linguist-generated=true +graph_type.rs linguist-generated=true half_size2d.rs linguist-generated=true half_size3d.rs linguist-generated=true image_buffer.rs linguist-generated=true diff --git a/crates/store/re_types/src/components/graph_edge.rs b/crates/store/re_types/src/components/graph_edge.rs new file mode 100644 index 000000000000..64dc08db7e52 --- /dev/null +++ b/crates/store/re_types/src/components/graph_edge.rs @@ -0,0 +1,105 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_edge.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: An edge in a graph connecting two nodes. +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct GraphEdge(pub crate::datatypes::Utf8Pair); + +impl ::re_types_core::SizeBytes for GraphEdge { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} + +impl> From for GraphEdge { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for GraphEdge { + #[inline] + fn borrow(&self) -> &crate::datatypes::Utf8Pair { + &self.0 + } +} + +impl std::ops::Deref for GraphEdge { + type Target = crate::datatypes::Utf8Pair; + + #[inline] + fn deref(&self) -> &crate::datatypes::Utf8Pair { + &self.0 + } +} + +impl std::ops::DerefMut for GraphEdge { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Utf8Pair { + &mut self.0 + } +} + +::re_types_core::macros::impl_into_cow!(GraphEdge); + +impl ::re_types_core::Loggable for GraphEdge { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + crate::datatypes::Utf8Pair::arrow_datatype() + } + + fn to_arrow2_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + crate::datatypes::Utf8Pair::to_arrow2_opt(data.into_iter().map(|datum| { + datum.map(|datum| match datum.into() { + ::std::borrow::Cow::Borrowed(datum) => ::std::borrow::Cow::Borrowed(&datum.0), + ::std::borrow::Cow::Owned(datum) => ::std::borrow::Cow::Owned(datum.0), + }) + })) + } + + fn from_arrow2_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + crate::datatypes::Utf8Pair::from_arrow2_opt(arrow_data) + .map(|v| v.into_iter().map(|v| v.map(Self)).collect()) + } +} + +impl ::re_types_core::Component for GraphEdge { + #[inline] + fn name() -> ComponentName { + "rerun.components.GraphEdge".into() + } +} diff --git a/crates/store/re_types/src/components/graph_node.rs b/crates/store/re_types/src/components/graph_node.rs new file mode 100644 index 000000000000..5e42187423bc --- /dev/null +++ b/crates/store/re_types/src/components/graph_node.rs @@ -0,0 +1,105 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_node.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: A string-based ID representing a node in a graph. +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct GraphNode(pub crate::datatypes::Utf8); + +impl ::re_types_core::SizeBytes for GraphNode { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} + +impl> From for GraphNode { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for GraphNode { + #[inline] + fn borrow(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::Deref for GraphNode { + type Target = crate::datatypes::Utf8; + + #[inline] + fn deref(&self) -> &crate::datatypes::Utf8 { + &self.0 + } +} + +impl std::ops::DerefMut for GraphNode { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Utf8 { + &mut self.0 + } +} + +::re_types_core::macros::impl_into_cow!(GraphNode); + +impl ::re_types_core::Loggable for GraphNode { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + crate::datatypes::Utf8::arrow_datatype() + } + + fn to_arrow2_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + crate::datatypes::Utf8::to_arrow2_opt(data.into_iter().map(|datum| { + datum.map(|datum| match datum.into() { + ::std::borrow::Cow::Borrowed(datum) => ::std::borrow::Cow::Borrowed(&datum.0), + ::std::borrow::Cow::Owned(datum) => ::std::borrow::Cow::Owned(datum.0), + }) + })) + } + + fn from_arrow2_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + crate::datatypes::Utf8::from_arrow2_opt(arrow_data) + .map(|v| v.into_iter().map(|v| v.map(Self)).collect()) + } +} + +impl ::re_types_core::Component for GraphNode { + #[inline] + fn name() -> ComponentName { + "rerun.components.GraphNode".into() + } +} diff --git a/crates/store/re_types/src/components/graph_node_ext.rs b/crates/store/re_types/src/components/graph_node_ext.rs new file mode 100644 index 000000000000..e45b48b77310 --- /dev/null +++ b/crates/store/re_types/src/components/graph_node_ext.rs @@ -0,0 +1,16 @@ +use super::GraphNode; + +impl GraphNode { + /// Returns the string slice of the graph node. + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl From for String { + #[inline] + fn from(value: GraphNode) -> Self { + value.as_str().to_owned() + } +} diff --git a/crates/store/re_types/src/components/graph_type.rs b/crates/store/re_types/src/components/graph_type.rs new file mode 100644 index 000000000000..38d5dd830701 --- /dev/null +++ b/crates/store/re_types/src/components/graph_type.rs @@ -0,0 +1,154 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_type.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(non_camel_case_types)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Specifies if a graph has directed or undirected edges. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] +#[repr(u8)] +pub enum GraphType { + /// The graph has undirected edges. + #[default] + Undirected = 1, + + /// The graph has directed edges. + Directed = 2, +} + +impl ::re_types_core::reflection::Enum for GraphType { + #[inline] + fn variants() -> &'static [Self] { + &[Self::Undirected, Self::Directed] + } + + #[inline] + fn docstring_md(self) -> &'static str { + match self { + Self::Undirected => "The graph has undirected edges.", + Self::Directed => "The graph has directed edges.", + } + } +} + +impl ::re_types_core::SizeBytes for GraphType { + #[inline] + fn heap_size_bytes(&self) -> u64 { + 0 + } + + #[inline] + fn is_pod() -> bool { + true + } +} + +impl std::fmt::Display for GraphType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Undirected => write!(f, "Undirected"), + Self::Directed => write!(f, "Directed"), + } + } +} + +::re_types_core::macros::impl_into_cow!(GraphType); + +impl ::re_types_core::Loggable for GraphType { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + #![allow(clippy::wildcard_imports)] + use arrow::datatypes::*; + DataType::UInt8 + } + + fn to_arrow2_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + #![allow(clippy::wildcard_imports)] + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow::datatypes::*; + use arrow2::array::*; + Ok({ + let (somes, data0): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + let datum = datum.map(|datum| *datum as u8); + (datum.is_some(), datum) + }) + .unzip(); + let data0_bitmap: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + PrimitiveArray::new( + Self::arrow_datatype().into(), + data0.into_iter().map(|v| v.unwrap_or_default()).collect(), + data0_bitmap, + ) + .boxed() + }) + } + + fn from_arrow2_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow::datatypes::*; + use arrow2::{array::*, buffer::*}; + Ok(arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.components.GraphType#enum")? + .into_iter() + .map(|opt| opt.copied()) + .map(|typ| match typ { + Some(1) => Ok(Some(Self::Undirected)), + Some(2) => Ok(Some(Self::Directed)), + None => Ok(None), + Some(invalid) => Err(DeserializationError::missing_union_arm( + Self::arrow_datatype(), + "", + invalid as _, + )), + }) + .collect::>>>() + .with_context("rerun.components.GraphType")?) + } +} + +impl ::re_types_core::Component for GraphType { + #[inline] + fn name() -> ComponentName { + "rerun.components.GraphType".into() + } +} diff --git a/crates/store/re_types/src/components/mod.rs b/crates/store/re_types/src/components/mod.rs index ee02251c6341..76ca71fd8475 100644 --- a/crates/store/re_types/src/components/mod.rs +++ b/crates/store/re_types/src/components/mod.rs @@ -26,6 +26,10 @@ mod gamma_correction; mod gamma_correction_ext; mod geo_line_string; mod geo_line_string_ext; +mod graph_edge; +mod graph_node; +mod graph_node_ext; +mod graph_type; mod half_size2d; mod half_size2d_ext; mod half_size3d; @@ -135,6 +139,9 @@ pub use self::fill_mode::FillMode; pub use self::fill_ratio::FillRatio; pub use self::gamma_correction::GammaCorrection; pub use self::geo_line_string::GeoLineString; +pub use self::graph_edge::GraphEdge; +pub use self::graph_node::GraphNode; +pub use self::graph_type::GraphType; pub use self::half_size2d::HalfSize2D; pub use self::half_size3d::HalfSize3D; pub use self::image_buffer::ImageBuffer; diff --git a/crates/store/re_types/src/datatypes/.gitattributes b/crates/store/re_types/src/datatypes/.gitattributes index 865e70adf065..978b395e02a9 100644 --- a/crates/store/re_types/src/datatypes/.gitattributes +++ b/crates/store/re_types/src/datatypes/.gitattributes @@ -27,6 +27,7 @@ tensor_data.rs linguist-generated=true tensor_dimension.rs linguist-generated=true tensor_dimension_index_selection.rs linguist-generated=true tensor_dimension_selection.rs linguist-generated=true +utf8pair.rs linguist-generated=true uuid.rs linguist-generated=true uvec2d.rs linguist-generated=true uvec3d.rs linguist-generated=true diff --git a/crates/store/re_types/src/datatypes/mod.rs b/crates/store/re_types/src/datatypes/mod.rs index 2c06e948ecfd..69cada396e56 100644 --- a/crates/store/re_types/src/datatypes/mod.rs +++ b/crates/store/re_types/src/datatypes/mod.rs @@ -49,6 +49,8 @@ mod tensor_dimension_ext; mod tensor_dimension_index_selection; mod tensor_dimension_selection; mod tensor_dimension_selection_ext; +mod utf8pair; +mod utf8pair_ext; mod uuid; mod uuid_ext; mod uvec2d; @@ -93,6 +95,7 @@ pub use self::tensor_data::TensorData; pub use self::tensor_dimension::TensorDimension; pub use self::tensor_dimension_index_selection::TensorDimensionIndexSelection; pub use self::tensor_dimension_selection::TensorDimensionSelection; +pub use self::utf8pair::Utf8Pair; pub use self::uuid::Uuid; pub use self::uvec2d::UVec2D; pub use self::uvec3d::UVec3D; diff --git a/crates/store/re_types/src/datatypes/utf8pair.rs b/crates/store/re_types/src/datatypes/utf8pair.rs new file mode 100644 index 000000000000..73105741330c --- /dev/null +++ b/crates/store/re_types/src/datatypes/utf8pair.rs @@ -0,0 +1,325 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Datatype**: Stores a tuple of UTF-8 strings. +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Utf8Pair { + /// The first string. + pub first: crate::datatypes::Utf8, + + /// The second string. + pub second: crate::datatypes::Utf8, +} + +impl ::re_types_core::SizeBytes for Utf8Pair { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.first.heap_size_bytes() + self.second.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() && ::is_pod() + } +} + +::re_types_core::macros::impl_into_cow!(Utf8Pair); + +impl ::re_types_core::Loggable for Utf8Pair { + #[inline] + fn arrow_datatype() -> arrow::datatypes::DataType { + #![allow(clippy::wildcard_imports)] + use arrow::datatypes::*; + DataType::Struct(Fields::from(vec![ + Field::new("first", ::arrow_datatype(), false), + Field::new("second", ::arrow_datatype(), false), + ])) + } + + fn to_arrow2_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + #![allow(clippy::wildcard_imports)] + #![allow(clippy::manual_is_variant_and)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow::datatypes::*; + use arrow2::array::*; + Ok({ + let (somes, data): (Vec<_>, Vec<_>) = data + .into_iter() + .map(|datum| { + let datum: Option<::std::borrow::Cow<'a, Self>> = datum.map(Into::into); + (datum.is_some(), datum) + }) + .unzip(); + let bitmap: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + StructArray::new( + Self::arrow_datatype().into(), + vec![ + { + let (somes, first): (Vec<_>, Vec<_>) = data + .iter() + .map(|datum| { + let datum = datum.as_ref().map(|datum| datum.first.clone()); + (datum.is_some(), datum) + }) + .unzip(); + let first_bitmap: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let offsets = arrow2::offset::Offsets::::try_from_lengths( + first.iter().map(|opt| { + opt.as_ref().map(|datum| datum.0.len()).unwrap_or_default() + }), + )? + .into(); + let inner_data: arrow2::buffer::Buffer = first + .into_iter() + .flatten() + .flat_map(|datum| datum.0 .0) + .collect(); + + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + unsafe { + Utf8Array::::new_unchecked( + DataType::Utf8.into(), + offsets, + inner_data, + first_bitmap, + ) + } + .boxed() + } + }, + { + let (somes, second): (Vec<_>, Vec<_>) = data + .iter() + .map(|datum| { + let datum = datum.as_ref().map(|datum| datum.second.clone()); + (datum.is_some(), datum) + }) + .unzip(); + let second_bitmap: Option = { + let any_nones = somes.iter().any(|some| !*some); + any_nones.then(|| somes.into()) + }; + { + let offsets = arrow2::offset::Offsets::::try_from_lengths( + second.iter().map(|opt| { + opt.as_ref().map(|datum| datum.0.len()).unwrap_or_default() + }), + )? + .into(); + let inner_data: arrow2::buffer::Buffer = second + .into_iter() + .flatten() + .flat_map(|datum| datum.0 .0) + .collect(); + + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + unsafe { + Utf8Array::::new_unchecked( + DataType::Utf8.into(), + offsets, + inner_data, + second_bitmap, + ) + } + .boxed() + } + }, + ], + bitmap, + ) + .boxed() + }) + } + + fn from_arrow2_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + #![allow(clippy::wildcard_imports)] + use ::re_types_core::{Loggable as _, ResultExt as _}; + use arrow::datatypes::*; + use arrow2::{array::*, buffer::*}; + Ok({ + let arrow_data = arrow_data + .as_any() + .downcast_ref::() + .ok_or_else(|| { + let expected = Self::arrow_datatype(); + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.Utf8Pair")?; + if arrow_data.is_empty() { + Vec::new() + } else { + let (arrow_data_fields, arrow_data_arrays) = + (arrow_data.fields(), arrow_data.values()); + let arrays_by_name: ::std::collections::HashMap<_, _> = arrow_data_fields + .iter() + .map(|field| field.name.as_str()) + .zip(arrow_data_arrays) + .collect(); + let first = { + if !arrays_by_name.contains_key("first") { + return Err(DeserializationError::missing_struct_field( + Self::arrow_datatype(), + "first", + )) + .with_context("rerun.datatypes.Utf8Pair"); + } + let arrow_data = &**arrays_by_name["first"]; + { + let arrow_data = arrow_data + .as_any() + .downcast_ref::>() + .ok_or_else(|| { + let expected = DataType::Utf8; + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.Utf8Pair#first")?; + let arrow_data_buf = arrow_data.values(); + let offsets = arrow_data.offsets(); + arrow2::bitmap::utils::ZipValidity::new_with_validity( + offsets.iter().zip(offsets.lengths()), + arrow_data.validity(), + ) + .map(|elem| { + elem.map(|(start, len)| { + let start = *start as usize; + let end = start + len; + if end > arrow_data_buf.len() { + return Err(DeserializationError::offset_slice_oob( + (start, end), + arrow_data_buf.len(), + )); + } + + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = + unsafe { arrow_data_buf.clone().sliced_unchecked(start, len) }; + Ok(data) + }) + .transpose() + }) + .map(|res_or_opt| { + res_or_opt.map(|res_or_opt| { + res_or_opt.map(|v| { + crate::datatypes::Utf8(::re_types_core::ArrowString(v)) + }) + }) + }) + .collect::>>>() + .with_context("rerun.datatypes.Utf8Pair#first")? + .into_iter() + } + }; + let second = { + if !arrays_by_name.contains_key("second") { + return Err(DeserializationError::missing_struct_field( + Self::arrow_datatype(), + "second", + )) + .with_context("rerun.datatypes.Utf8Pair"); + } + let arrow_data = &**arrays_by_name["second"]; + { + let arrow_data = arrow_data + .as_any() + .downcast_ref::>() + .ok_or_else(|| { + let expected = DataType::Utf8; + let actual = arrow_data.data_type().clone(); + DeserializationError::datatype_mismatch(expected, actual) + }) + .with_context("rerun.datatypes.Utf8Pair#second")?; + let arrow_data_buf = arrow_data.values(); + let offsets = arrow_data.offsets(); + arrow2::bitmap::utils::ZipValidity::new_with_validity( + offsets.iter().zip(offsets.lengths()), + arrow_data.validity(), + ) + .map(|elem| { + elem.map(|(start, len)| { + let start = *start as usize; + let end = start + len; + if end > arrow_data_buf.len() { + return Err(DeserializationError::offset_slice_oob( + (start, end), + arrow_data_buf.len(), + )); + } + + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + let data = + unsafe { arrow_data_buf.clone().sliced_unchecked(start, len) }; + Ok(data) + }) + .transpose() + }) + .map(|res_or_opt| { + res_or_opt.map(|res_or_opt| { + res_or_opt.map(|v| { + crate::datatypes::Utf8(::re_types_core::ArrowString(v)) + }) + }) + }) + .collect::>>>() + .with_context("rerun.datatypes.Utf8Pair#second")? + .into_iter() + } + }; + arrow2::bitmap::utils::ZipValidity::new_with_validity( + ::itertools::izip!(first, second), + arrow_data.validity(), + ) + .map(|opt| { + opt.map(|(first, second)| { + Ok(Self { + first: first + .ok_or_else(DeserializationError::missing_data) + .with_context("rerun.datatypes.Utf8Pair#first")?, + second: second + .ok_or_else(DeserializationError::missing_data) + .with_context("rerun.datatypes.Utf8Pair#second")?, + }) + }) + .transpose() + }) + .collect::>>() + .with_context("rerun.datatypes.Utf8Pair")? + } + }) + } +} diff --git a/crates/store/re_types/src/datatypes/utf8pair_ext.rs b/crates/store/re_types/src/datatypes/utf8pair_ext.rs new file mode 100644 index 000000000000..3bcaa04d5a43 --- /dev/null +++ b/crates/store/re_types/src/datatypes/utf8pair_ext.rs @@ -0,0 +1,21 @@ +use crate::datatypes::Utf8; + +impl> From<(T, T)> for super::Utf8Pair { + #[inline(always)] + fn from(value: (T, T)) -> Self { + Self { + first: value.0.into(), + second: value.1.into(), + } + } +} + +impl From<&(&str, &str)> for super::Utf8Pair { + #[inline(always)] + fn from(value: &(&str, &str)) -> Self { + Self { + first: value.0.into(), + second: value.1.into(), + } + } +} diff --git a/crates/viewer/re_component_ui/src/lib.rs b/crates/viewer/re_component_ui/src/lib.rs index 7d8d0577ccde..f5ac978d76f7 100644 --- a/crates/viewer/re_component_ui/src/lib.rs +++ b/crates/viewer/re_component_ui/src/lib.rs @@ -37,9 +37,9 @@ use re_types::{ }, components::{ AggregationPolicy, AlbedoFactor, AxisLength, Color, DepthMeter, DrawOrder, FillMode, - FillRatio, GammaCorrection, ImagePlaneDistance, MagnificationFilter, MarkerSize, Name, - Opacity, Range1D, Scale3D, ShowLabels, StrokeWidth, Text, TransformRelation, Translation3D, - ValueRange, + FillRatio, GammaCorrection, GraphType, ImagePlaneDistance, MagnificationFilter, MarkerSize, + Name, Opacity, Range1D, Scale3D, ShowLabels, StrokeWidth, Text, TransformRelation, + Translation3D, ValueRange, }, Component as _, }; @@ -103,6 +103,7 @@ pub fn create_component_ui_registry() -> re_viewer_context::ComponentUiRegistry registry.add_singleline_edit_or_view::(edit_view_enum); registry.add_singleline_edit_or_view::(edit_view_enum); registry.add_singleline_edit_or_view::(edit_view_enum); + registry.add_singleline_edit_or_view::(edit_view_enum); registry.add_singleline_edit_or_view::(edit_view_enum); registry.add_singleline_edit_or_view::( edit_view_enum_with_variant_available::< diff --git a/crates/viewer/re_space_view_graph/Cargo.toml b/crates/viewer/re_space_view_graph/Cargo.toml new file mode 100644 index 000000000000..d049ad1e1285 --- /dev/null +++ b/crates/viewer/re_space_view_graph/Cargo.toml @@ -0,0 +1,38 @@ +[package] +authors.workspace = true +description = "A space view that shows a graph (node-link diagram)." +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "re_space_view_graph" +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +re_chunk.workspace = true +re_format.workspace = true +re_log_types.workspace = true +re_query.workspace = true +re_renderer.workspace = true +re_space_view.workspace = true +re_tracing.workspace = true +re_types.workspace = true +re_ui.workspace = true +re_viewer_context.workspace = true +re_viewport_blueprint.workspace = true + +ahash.workspace = true +bytemuck.workspace = true +egui.workspace = true +fjadra.workspace = true +nohash-hasher.workspace = true diff --git a/crates/viewer/re_space_view_graph/README.md b/crates/viewer/re_space_view_graph/README.md new file mode 100644 index 000000000000..258ce182e6d4 --- /dev/null +++ b/crates/viewer/re_space_view_graph/README.md @@ -0,0 +1,11 @@ +# re_space_view_graph + +Part of the [`rerun`](https://github.com/rerun-io/rerun?speculative-link) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_space_view_graph.svg)](https://crates.io/crates/re_space_view_graph?speculative-link) +[![Documentation](https://docs.rs/re_space_view_graph/badge.svg)](https://docs.rs/re_space_view_graph?speculative-link) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +A Space View that shows graphs (node-link diagrams). + diff --git a/crates/viewer/re_space_view_graph/src/graph/hash.rs b/crates/viewer/re_space_view_graph/src/graph/hash.rs new file mode 100644 index 000000000000..4d8e430de571 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/graph/hash.rs @@ -0,0 +1,43 @@ +use re_log_types::hash::Hash64; +use re_types::components; + +/// A 64 bit hash of [`components::GraphNode`] with very small risk of collision. +#[derive(Copy, Clone, Eq, PartialOrd, Ord)] +pub struct GraphNodeHash(Hash64); + +impl nohash_hasher::IsEnabled for GraphNodeHash {} + +impl GraphNodeHash { + #[inline] + pub fn hash64(&self) -> u64 { + self.0.hash64() + } +} + +// We implement `Hash` manually, because `nohash_hasher` requires a single call to `state.write_*`. +// More info: https://crates.io/crates/nohash-hasher +impl std::hash::Hash for GraphNodeHash { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u64(self.hash64()); + } +} + +impl std::fmt::Debug for GraphNodeHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NodeIdHash({:016X})", self.hash64()) + } +} + +impl From<&components::GraphNode> for GraphNodeHash { + fn from(node_id: &components::GraphNode) -> Self { + Self(Hash64::hash(node_id)) + } +} + +impl std::cmp::PartialEq for GraphNodeHash { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} diff --git a/crates/viewer/re_space_view_graph/src/graph/index.rs b/crates/viewer/re_space_view_graph/src/graph/index.rs new file mode 100644 index 000000000000..10db2dcf7d0d --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/graph/index.rs @@ -0,0 +1,37 @@ +use re_log_types::{EntityPath, EntityPathHash}; +use re_types::components; + +use super::GraphNodeHash; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct NodeIndex { + pub entity_hash: EntityPathHash, + pub node_hash: GraphNodeHash, +} + +impl nohash_hasher::IsEnabled for NodeIndex {} + +// We implement `Hash` manually, because `nohash_hasher` requires a single call to `state.write_*`. +// More info: https://crates.io/crates/nohash-hasher +impl std::hash::Hash for NodeIndex { + fn hash(&self, state: &mut H) { + // TODO(grtlr): Consider using `write_usize` here, to further decrease the risk of collision. + let combined = self.entity_hash.hash64() << 32 | self.node_hash.hash64(); + state.write_u64(combined); + } +} + +impl NodeIndex { + pub fn from_entity_node(entity_path: &EntityPath, node: &components::GraphNode) -> Self { + Self { + entity_hash: entity_path.hash(), + node_hash: GraphNodeHash::from(node), + } + } +} + +impl std::fmt::Debug for NodeIndex { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "NodeIndex({:?}@{:?})", self.node_hash, self.entity_hash) + } +} diff --git a/crates/viewer/re_space_view_graph/src/graph/mod.rs b/crates/viewer/re_space_view_graph/src/graph/mod.rs new file mode 100644 index 000000000000..faf9a80f0d97 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/graph/mod.rs @@ -0,0 +1,87 @@ +mod hash; +pub(crate) use hash::GraphNodeHash; +mod index; +pub(crate) use index::NodeIndex; + +use re_types::components::{GraphNode, GraphType}; + +use crate::visualizers::{EdgeData, EdgeInstance, NodeData, NodeInstance}; + +pub struct NodeInstanceImplicit { + pub node: GraphNode, + pub index: NodeIndex, +} + +impl std::hash::Hash for NodeInstanceImplicit { + fn hash(&self, state: &mut H) { + self.index.hash(state); + } +} + +#[derive(Hash)] +pub struct Graph<'a> { + explicit: &'a [NodeInstance], + implicit: Vec, + edges: &'a [EdgeInstance], + kind: GraphType, +} + +impl<'a> Graph<'a> { + pub fn new(node_data: Option<&'a NodeData>, edge_data: Option<&'a EdgeData>) -> Self { + // We keep track of the nodes to find implicit nodes. + let mut seen = ahash::HashSet::default(); + + let explicit = if let Some(data) = node_data { + seen.extend(data.nodes.iter().map(|n| n.index)); + data.nodes.as_slice() + } else { + &[][..] + }; + + let (edges, implicit, kind) = if let Some(data) = edge_data { + let mut implicit = Vec::new(); + for edge in &data.edges { + if !seen.contains(&edge.source_index) { + implicit.push(NodeInstanceImplicit { + node: edge.source.clone(), + index: edge.source_index, + }); + seen.insert(edge.source_index); + } + if !seen.contains(&edge.target_index) { + implicit.push(NodeInstanceImplicit { + node: edge.target.clone(), + index: edge.target_index, + }); + seen.insert(edge.target_index); + } + } + (data.edges.as_slice(), implicit, Some(data.graph_type)) + } else { + (&[][..], Vec::new(), None) + }; + + Self { + explicit, + implicit, + edges, + kind: kind.unwrap_or_default(), + } + } + + pub fn nodes_explicit(&self) -> impl Iterator { + self.explicit.iter() + } + + pub fn nodes_implicit(&self) -> impl Iterator + '_ { + self.implicit.iter() + } + + pub fn edges(&self) -> impl Iterator { + self.edges.iter() + } + + pub fn kind(&self) -> GraphType { + self.kind + } +} diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs new file mode 100644 index 000000000000..0b4a994c329c --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -0,0 +1,129 @@ +use egui::{Pos2, Rect, Vec2}; +use fjadra as fj; + +use crate::{ + graph::{Graph, NodeIndex}, + ui::bounding_rect_from_iter, + visualizers::NodeInstance, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Layout { + extents: ahash::HashMap, +} + +impl Layout { + pub fn bounding_rect(&self) -> Rect { + bounding_rect_from_iter(self.extents.values().copied()) + } + + /// Gets the position and size of a node in the layout. + pub fn get(&self, node: &NodeIndex) -> Option { + self.extents.get(node).copied() + } + + /// Updates the size and position of a node, for example after size changes. + /// Returns `true` if the node changed its size. + pub fn update(&mut self, node: &NodeIndex, rect: Rect) -> bool { + debug_assert!( + self.extents.contains_key(node), + "node should exist in the layout" + ); + if let Some(extent) = self.extents.get_mut(node) { + let size_changed = (extent.size() - rect.size()).length_sq() > 0.01; + *extent = rect; + return size_changed; + } + false + } +} + +impl<'a> From<&'a NodeInstance> for fj::Node { + fn from(instance: &'a NodeInstance) -> Self { + let mut node = Self::default(); + if let Some(pos) = instance.position { + node = node.fixed_position(pos.x as f64, pos.y as f64); + } + node + } +} + +pub struct ForceLayout { + simulation: fj::Simulation, + node_index: ahash::HashMap, +} + +impl ForceLayout { + pub fn new<'a>(graphs: impl Iterator> + Clone) -> Self { + let explicit = graphs + .clone() + .flat_map(|g| g.nodes_explicit().map(|n| (n.index, fj::Node::from(n)))); + let implicit = graphs + .clone() + .flat_map(|g| g.nodes_implicit().map(|n| (n.index, fj::Node::default()))); + + let mut node_index = ahash::HashMap::default(); + let all_nodes: Vec = explicit + .chain(implicit) + .enumerate() + .map(|(i, n)| { + node_index.insert(n.0, i); + n.1 + }) + .collect(); + + let all_edges = graphs.flat_map(|g| { + g.edges() + .map(|e| (node_index[&e.source_index], node_index[&e.target_index])) + }); + + // TODO(grtlr): Currently we guesstimate good forces. Eventually these should be exposed as blueprints. + let simulation = fj::SimulationBuilder::default() + .with_alpha_decay(0.01) // TODO(grtlr): slows down the simulation for demo + .build(all_nodes) + .add_force( + "link", + fj::Link::new(all_edges).distance(50.0).iterations(2), + ) + .add_force("charge", fj::ManyBody::new()) + // TODO(grtlr): This is a small stop-gap until we have blueprints to prevent nodes from flying away. + .add_force("x", fj::PositionX::new().strength(0.01)) + .add_force("y", fj::PositionY::new().strength(0.01)); + + Self { + simulation, + node_index, + } + } + + pub fn init(&self) -> Layout { + let positions = self.simulation.positions().collect::>(); + let mut extents = ahash::HashMap::default(); + + for (node, i) in &self.node_index { + let [x, y] = positions[*i]; + let pos = Pos2::new(x as f32, y as f32); + let size = Vec2::ZERO; + let rect = Rect::from_min_size(pos, size); + extents.insert(*node, rect); + } + + Layout { extents } + } + + /// Returns `true` if finished. + pub fn tick(&mut self, layout: &mut Layout) -> bool { + self.simulation.tick(1); + + let positions = self.simulation.positions().collect::>(); + + for (node, extent) in &mut layout.extents { + let i = self.node_index[node]; + let [x, y] = positions[i]; + let pos = Pos2::new(x as f32, y as f32); + extent.set_center(pos); + } + + self.simulation.finished() + } +} diff --git a/crates/viewer/re_space_view_graph/src/lib.rs b/crates/viewer/re_space_view_graph/src/lib.rs new file mode 100644 index 000000000000..17634033ab30 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/lib.rs @@ -0,0 +1,12 @@ +//! Rerun Graph Space View. +//! +//! A Space View that shows a graph (node-link diagram). + +mod graph; +mod layout; +mod properties; +mod ui; +mod view; +mod visualizers; + +pub use view::GraphSpaceView; diff --git a/crates/viewer/re_space_view_graph/src/properties.rs b/crates/viewer/re_space_view_graph/src/properties.rs new file mode 100644 index 000000000000..de6b0255f0ca --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/properties.rs @@ -0,0 +1,23 @@ +use re_types::blueprint::components::VisualBounds2D; +use re_viewer_context::{SpaceViewStateExt as _, TypedComponentFallbackProvider}; + +use crate::{ui::GraphSpaceViewState, GraphSpaceView}; + +fn valid_bound(rect: &egui::Rect) -> bool { + rect.is_finite() && rect.is_positive() +} + +impl TypedComponentFallbackProvider for GraphSpaceView { + fn fallback_for(&self, ctx: &re_viewer_context::QueryContext<'_>) -> VisualBounds2D { + let Ok(state) = ctx.view_state.downcast_ref::() else { + return VisualBounds2D::default(); + }; + + match state.layout_state.bounding_rect() { + Some(rect) if valid_bound(&rect) => rect.into(), + _ => VisualBounds2D::default(), + } + } +} + +re_viewer_context::impl_component_fallback_provider!(GraphSpaceView => [VisualBounds2D]); diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/edge.rs b/crates/viewer/re_space_view_graph/src/ui/draw/edge.rs new file mode 100644 index 000000000000..501614fe7655 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/draw/edge.rs @@ -0,0 +1,67 @@ +use egui::{Color32, Frame, Painter, Pos2, Rect, Response, Shape, Stroke, Ui, Vec2}; + +/// Draws an edge with an optional arrow mark at the target end. +pub fn draw_edge(ui: &mut Ui, source: Rect, target: Rect, show_arrow: bool) -> Response { + let fg = ui.style().visuals.text_color(); + + Frame::default() + .show(ui, |ui| { + let source_center = source.center(); + let target_center = target.center(); + + // Calculate direction vector from source to target + let direction = (target_center - source_center).normalized(); + + // Find the border points on both rectangles + let source_point = find_border_point(source, -direction); // Reverse direction for target + let target_point = find_border_point(target, direction); + + let painter = ui.painter(); + + painter.line_segment([source_point, target_point], Stroke::new(1.0, fg)); + + // Conditionally draw an arrow at the target point + if show_arrow { + draw_arrow(painter, target_point, direction, fg); + } + }) + .response +} + +/// Helper function to find the point where the line intersects the border of a rectangle +fn find_border_point(rect: Rect, direction: Vec2) -> Pos2 { + let mut t_min = f32::NEG_INFINITY; + let mut t_max = f32::INFINITY; + + for i in 0..2 { + let inv_d = 1.0 / direction[i]; + let mut t0 = (rect.min[i] - rect.center()[i]) * inv_d; + let mut t1 = (rect.max[i] - rect.center()[i]) * inv_d; + + if inv_d < 0.0 { + std::mem::swap(&mut t0, &mut t1); + } + + t_min = t_min.max(t0); + t_max = t_max.min(t1); + } + + let t = t_max.min(t_min); // Pick the first intersection + rect.center() + t * direction +} + +/// Helper function to draw an arrow at the end of the edge +fn draw_arrow(painter: &Painter, tip: Pos2, direction: Vec2, color: Color32) { + let arrow_size = 10.0; // Adjust size as needed + let perpendicular = Vec2::new(-direction.y, direction.x) * 0.5 * arrow_size; + + let p1 = tip - direction * arrow_size + perpendicular; + let p2 = tip - direction * arrow_size - perpendicular; + + // Draw a filled triangle for the arrow + painter.add(Shape::convex_polygon( + vec![tip, p1, p2], + color, + Stroke::NONE, + )); +} diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/entity.rs b/crates/viewer/re_space_view_graph/src/ui/draw/entity.rs new file mode 100644 index 000000000000..91a97e4b5972 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/draw/entity.rs @@ -0,0 +1,44 @@ +use egui::{Align2, Color32, FontId, Rect, Response, Sense, Stroke, Ui}; + +use re_log_types::EntityPath; +use re_viewer_context::SpaceViewHighlights; + +pub fn draw_entity( + ui: &mut Ui, + rect: Rect, + entity_path: &EntityPath, + highlights: &SpaceViewHighlights, +) -> Response { + let (rect, response) = ui.allocate_at_least(rect.size(), Sense::drag()); + + let color = if highlights + .entity_outline_mask(entity_path.hash()) + .overall + .is_some() + { + ui.ctx().style().visuals.text_color() + } else { + ui.ctx() + .style() + .visuals + .gray_out(ui.ctx().style().visuals.text_color()) + }; + + let padded = rect.expand(10.0); + + ui.painter() + .rect(padded, 0.0, Color32::TRANSPARENT, Stroke::new(1.0, color)); + + ui.painter().text( + padded.left_top(), + Align2::LEFT_BOTTOM, + entity_path.to_string(), + FontId { + size: 12.0, + family: Default::default(), + }, + color, + ); + + response +} diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs b/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs new file mode 100644 index 000000000000..4bf84020d259 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/draw/mod.rs @@ -0,0 +1,7 @@ +mod edge; +mod entity; +mod node; + +pub use edge::draw_edge; +pub use entity::draw_entity; +pub use node::{draw_explicit, draw_implicit}; diff --git a/crates/viewer/re_space_view_graph/src/ui/draw/node.rs b/crates/viewer/re_space_view_graph/src/ui/draw/node.rs new file mode 100644 index 000000000000..0ede98194065 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/draw/node.rs @@ -0,0 +1,83 @@ +use egui::{Frame, Label, Response, RichText, Sense, Stroke, TextWrapMode, Ui, Vec2}; +use re_viewer_context::{InteractionHighlight, SelectionHighlight}; + +use crate::{graph::NodeInstanceImplicit, ui::scene::SceneContext, visualizers::NodeInstance}; + +/// The `world_to_ui_scale` parameter is used to convert between world and ui coordinates. +pub fn draw_explicit( + ui: &mut Ui, + ctx: &SceneContext, + node: &NodeInstance, + highlight: InteractionHighlight, +) -> Response { + let visuals = &ui.style().visuals; + + let fg = node.color.unwrap_or_else(|| visuals.text_color()); + + let response = if let (Some(label), true) = (node.label.as_ref(), node.show_label) { + // Draw a text node. + + let bg = visuals.widgets.noninteractive.bg_fill; + let stroke = match highlight.selection { + SelectionHighlight::Selection => visuals.selection.stroke, + _ => Stroke::new(1.0, visuals.text_color()), + }; + + let text = RichText::new(label.as_str()).color(fg); + let label = Label::new(text).wrap_mode(TextWrapMode::Extend); + + Frame::default() + .rounding(4.0) + .stroke(stroke) + .inner_margin(Vec2::new(6.0, 4.0)) + .fill(bg) + .show(ui, |ui| ui.add(label)) + .response + } else { + // Draw a circle node. + let r = node.radius.map(|r| ctx.radius_to_world(r)).unwrap_or(4.0); + debug_assert!(r.is_sign_positive(), "radius must be greater than zero"); + + Frame::default() + .show(ui, |ui| { + ui.add(|ui: &mut Ui| { + let (rect, response) = ui.allocate_at_least( + Vec2::splat(2.0 * r), + Sense::hover(), // Change this to allow dragging. + ); // Frame size + ui.painter().circle(rect.center(), r, fg, Stroke::NONE); + response + }) + }) + .response + }; + + if let Some(label) = node.label.as_ref() { + response.on_hover_text(format!( + "Graph Node: {}\nLabel: {label}", + node.node.as_str(), + )) + } else { + response.on_hover_text(format!("Graph Node: {}", node.node.as_str(),)) + } +} + +/// Draws an implicit node instance (dummy node). +pub fn draw_implicit(ui: &mut egui::Ui, node: &NodeInstanceImplicit) -> Response { + let fg = ui.style().visuals.gray_out(ui.style().visuals.text_color()); + let r = 4.0; + + Frame::default() + .show(ui, |ui| { + ui.add(|ui: &mut Ui| { + let (rect, response) = ui.allocate_at_least( + Vec2::splat(2.0 * r), + Sense::hover(), // Change this to allow dragging. + ); // Frame size + ui.painter().circle(rect.center(), r, fg, Stroke::NONE); + response + }) + }) + .response + .on_hover_text(format!("Implicit Node: `{}`", node.node.as_str(),)) +} diff --git a/crates/viewer/re_space_view_graph/src/ui/mod.rs b/crates/viewer/re_space_view_graph/src/ui/mod.rs new file mode 100644 index 000000000000..aa7590ecd9d7 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/mod.rs @@ -0,0 +1,10 @@ +mod draw; +mod state; + +pub mod scene; + +pub use state::{Discriminator, GraphSpaceViewState}; + +pub fn bounding_rect_from_iter(rectangles: impl Iterator) -> egui::Rect { + rectangles.fold(egui::Rect::NOTHING, |acc, rect| acc.union(rect)) +} diff --git a/crates/viewer/re_space_view_graph/src/ui/scene.rs b/crates/viewer/re_space_view_graph/src/ui/scene.rs new file mode 100644 index 000000000000..f1de6e15746b --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/scene.rs @@ -0,0 +1,276 @@ +use egui::{ + emath::TSTransform, Area, Color32, Id, LayerId, Order, Painter, Pos2, Rect, Response, Sense, + Stroke, Ui, Vec2, +}; +use re_chunk::EntityPath; +use re_types::{components::Radius, datatypes::Float32}; +use re_viewer_context::{InteractionHighlight, SpaceViewHighlights}; +use std::hash::Hash; + +use crate::{ + graph::NodeInstanceImplicit, + visualizers::{EdgeInstance, NodeInstance}, +}; + +use super::draw::{draw_edge, draw_entity, draw_explicit, draw_implicit}; + +fn fit_to_world_rect(clip_rect_window: Rect, world_rect: Rect) -> TSTransform { + let available_size = clip_rect_window.size(); + + // Compute the scale factor to fit the bounding rectangle into the available screen size. + let scale_x = available_size.x / world_rect.width(); + let scale_y = available_size.y / world_rect.height(); + + // Use the smaller of the two scales to ensure the whole rectangle fits on the screen. + let scale = scale_x.min(scale_y).min(1.0); + + // Compute the translation to center the bounding rect in the screen. + let center_screen = Pos2::new(available_size.x / 2.0, available_size.y / 2.0); + let center_world = world_rect.center().to_vec2(); + + // Set the transformation to scale and then translate to center. + + TSTransform::from_translation(center_screen.to_vec2() - center_world * scale) + * TSTransform::from_scaling(scale) +} + +pub struct SceneBuilder { + show_debug: bool, + world_bounds: Rect, + bounding_rect: Rect, + children_drag_delta: Vec2, + children_hovered: bool, +} + +impl SceneBuilder { + pub fn from_world_bounds(world_bounds: impl Into) -> Self { + Self { + world_bounds: world_bounds.into(), + show_debug: false, + bounding_rect: Rect::NOTHING, + children_drag_delta: Vec2::ZERO, + children_hovered: false, + } + } + + pub fn show_debug(&mut self) { + self.show_debug = true; + } + + /// Return the clip rect of the scene in window coordinates. + pub fn add(mut self, ui: &mut Ui, add_scene_contents: F) -> (Rect, Response) + where + F: for<'b> FnOnce(Scene<'b>), + { + re_tracing::profile_function!(); + + let (id, clip_rect_window) = ui.allocate_space(ui.available_size()); + let response = ui.interact(clip_rect_window, id, Sense::click_and_drag()); + + let mut world_to_view = fit_to_world_rect(clip_rect_window, self.world_bounds); + + let view_to_window = TSTransform::from_translation(ui.min_rect().left_top().to_vec2()); + let world_to_window = view_to_window * world_to_view; + let clip_rect_world = world_to_window.inverse() * clip_rect_window; + + let window_layer = ui.layer_id(); + + add_scene_contents(Scene { + ui, + id, + window_layer, + context: SceneContext { + clip_rect_world, + world_to_window, + }, + bounding_rect: &mut self.bounding_rect, + children_drag_delta: &mut self.children_drag_delta, + children_hovered: &mut self.children_hovered, + }); + + // :warning: Currently, `children_drag_delta` and `children_hovered` only report events from the entity rectangles. + // TODO(grtlr): Would it makes sense to move the creation of the `Response` here and let `Canvas` only be a thin wrapper? + // That way we would avoid the duplication between those objects and we would have single-responsibility. + + world_to_view.translation += self.children_drag_delta; + if response.dragged() { + world_to_view.translation += response.drag_delta(); + } + + if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) { + // Note: Catch zooming / panning either in the container, or on the entities. + if response.hovered() || self.children_hovered { + let pointer_in_world = world_to_window.inverse() * pointer; + let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); + let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta); + + // Zoom in on pointer, but only if we are not zoomed out too far. + if zoom_delta < 1.0 || world_to_view.scaling < 1.0 { + world_to_view = world_to_view + * TSTransform::from_translation(pointer_in_world.to_vec2()) + * TSTransform::from_scaling(zoom_delta) + * TSTransform::from_translation(-pointer_in_world.to_vec2()); + } + + // Pan: + world_to_view = TSTransform::from_translation(pan_delta) * world_to_view; + } + } + + // We need to draw the debug information after the rest to ensure that we have the correct bounding box. + if self.show_debug { + let debug_id = LayerId::new(Order::Debug, id.with("debug_layer")); + ui.ctx().set_transform_layer(debug_id, world_to_window); + + // Paint the coordinate system. + let painter = Painter::new(ui.ctx().clone(), debug_id, clip_rect_world); + + // paint coordinate system at the world origin + let origin = Pos2::new(0.0, 0.0); + let x_axis = Pos2::new(100.0, 0.0); + let y_axis = Pos2::new(0.0, 100.0); + + painter.line_segment([origin, x_axis], Stroke::new(1.0, Color32::RED)); + painter.line_segment([origin, y_axis], Stroke::new(2.0, Color32::GREEN)); + + if self.bounding_rect.is_positive() { + painter.rect( + self.bounding_rect, + 0.0, + Color32::from_rgba_unmultiplied(255, 0, 255, 8), + Stroke::new(1.0, Color32::from_rgb(255, 0, 255)), + ); + } + } + + ( + (view_to_window * world_to_view).inverse() * clip_rect_window, + response, + ) + } +} + +pub struct SceneContext { + clip_rect_world: Rect, + world_to_window: TSTransform, +} + +impl SceneContext { + pub fn distance_to_world(&self, distance: f32) -> f32 { + distance / self.world_to_window.scaling + } + + /// If the radius is negative, we need to convert it from ui to world coordinates. + pub fn radius_to_world(&self, radius: Radius) -> f32 { + match radius { + Radius(Float32(r)) if r.is_sign_positive() => r, + Radius(Float32(r)) => self.distance_to_world(r.abs()), + } + } +} + +pub struct Scene<'a> { + ui: &'a mut Ui, + id: Id, + window_layer: LayerId, + context: SceneContext, + bounding_rect: &'a mut Rect, + children_drag_delta: &'a mut Vec2, + children_hovered: &'a mut bool, +} + +impl<'a> Scene<'a> { + /// Draws a regular node, i.e. an explicit node instance. + pub fn explicit_node( + &mut self, + pos: Pos2, + node: &NodeInstance, + highlights: InteractionHighlight, + ) -> Response { + self.node_wrapper(node.index, pos, |ui, world_to_ui| { + draw_explicit(ui, world_to_ui, node, highlights) + }) + } + + pub fn implicit_node(&mut self, pos: Pos2, node: &NodeInstanceImplicit) -> Response { + self.node_wrapper(node.index, pos, |ui, _| draw_implicit(ui, node)) + } + + /// `pos` is the top-left position of the node in world coordinates. + fn node_wrapper(&mut self, id: impl Hash, pos: Pos2, add_node_contents: F) -> Response + where + F: for<'b> FnOnce(&'b mut Ui, &'b SceneContext) -> Response, + { + let response = Area::new(self.id.with(id)) + .fixed_pos(pos) + .order(Order::Middle) + .constrain(false) + .show(self.ui.ctx(), |ui| { + ui.set_clip_rect(self.context.clip_rect_world); + add_node_contents(ui, &self.context) + }) + .response; + + let id = response.layer_id; + self.ui + .ctx() + .set_transform_layer(id, self.context.world_to_window); + self.ui.ctx().set_sublayer(self.window_layer, id); + + *self.bounding_rect = self.bounding_rect.union(response.rect); + + response + } + + pub fn entity( + &mut self, + entity: &EntityPath, + rect: Rect, + highlights: &SpaceViewHighlights, + ) -> Response { + let response = Area::new(self.id.with(entity)) + .fixed_pos(rect.min) + .order(Order::Background) + .constrain(false) + .show(self.ui.ctx(), |ui| { + ui.set_clip_rect(self.context.clip_rect_world); + draw_entity(ui, rect, entity, highlights) + }) + .inner; + + if response.dragged() { + *self.children_drag_delta += response.drag_delta(); + } + + if response.hovered() { + *self.children_hovered = true; + } + + let id = response.layer_id; + self.ui + .ctx() + .set_transform_layer(id, self.context.world_to_window); + self.ui.ctx().set_sublayer(self.window_layer, id); + + response + } + + pub fn edge(&self, from: Rect, to: Rect, edge: &EdgeInstance, show_arrow: bool) -> Response { + let response = Area::new(self.id.with(((edge.source_index, edge.target_index),))) + .order(Order::Middle) + .constrain(false) + .show(self.ui.ctx(), |ui| { + ui.set_clip_rect(self.context.clip_rect_world); + draw_edge(ui, from, to, show_arrow) + }) + .response; + + let id = response.layer_id; + self.ui + .ctx() + .set_transform_layer(id, self.context.world_to_window); + self.ui.ctx().set_sublayer(self.window_layer, id); + + response + } +} diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs new file mode 100644 index 000000000000..21cbe3dabce1 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -0,0 +1,185 @@ +use egui::Rect; +use re_format::format_f32; +use re_types::blueprint::components::VisualBounds2D; +use re_ui::UiExt; +use re_viewer_context::SpaceViewState; + +use crate::{ + graph::Graph, + layout::{ForceLayout, Layout}, +}; + +/// Space view state for the custom space view. +/// +/// This state is preserved between frames, but not across Viewer sessions. +#[derive(Default)] +pub struct GraphSpaceViewState { + pub layout_state: LayoutState, + + pub show_debug: bool, + + pub world_bounds: Option, +} + +impl GraphSpaceViewState { + pub fn layout_ui(&self, ui: &mut egui::Ui) { + let Some(rect) = self.layout_state.bounding_rect() else { + return; + }; + ui.grid_left_hand_label("Layout") + .on_hover_text("The bounding box encompassing all entities in the view right now"); + ui.vertical(|ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + let egui::Rect { min, max } = rect; + ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); + ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); + }); + ui.end_row(); + } + + pub fn debug_ui(&mut self, ui: &mut egui::Ui) { + ui.re_checkbox(&mut self.show_debug, "Show debug information") + .on_hover_text("Shows debug information for the current graph"); + ui.end_row(); + } + + pub fn simulation_ui(&mut self, ui: &mut egui::Ui) { + if ui.button("Reset simulation").clicked() { + self.layout_state.reset(); + } + } +} + +impl SpaceViewState for GraphSpaceViewState { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + +/// Used to determine if a layout is up-to-date or outdated. For now we use a +/// hash of the data the comes from the visualizers. +#[derive(Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct Discriminator(u64); + +impl Discriminator { + pub fn new(hash: u64) -> Self { + Self(hash) + } +} + +/// The following is a simple state machine that keeps track of the different +/// layouts and if they need to be recomputed. It also holds the state of the +/// force-based simulation. +#[derive(Default)] +pub enum LayoutState { + #[default] + None, + InProgress { + discriminator: Discriminator, + layout: Layout, + provider: ForceLayout, + }, + Finished { + discriminator: Discriminator, + layout: Layout, + _provider: ForceLayout, + }, +} + +impl LayoutState { + pub fn bounding_rect(&self) -> Option { + match self { + Self::None => None, + Self::Finished { layout, .. } | Self::InProgress { layout, .. } => { + Some(layout.bounding_rect()) + } + } + } + + pub fn reset(&mut self) { + *self = Self::None; + } + + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + pub fn is_in_progress(&self) -> bool { + matches!(self, Self::InProgress { .. }) + } + + /// A simple state machine that keeps track of the different stages and if the layout needs to be recomputed. + fn update<'a>( + self, + requested: Discriminator, + graphs: impl Iterator> + Clone, + ) -> Self { + match self { + // Layout is up to date, nothing to do here. + Self::Finished { + ref discriminator, .. + } if discriminator == &requested => { + self // no op + } + // We need to recompute the layout. + Self::None | Self::Finished { .. } => { + let provider = ForceLayout::new(graphs); + let layout = provider.init(); + + Self::InProgress { + discriminator: requested, + layout, + provider, + } + } + Self::InProgress { + ref discriminator, .. + } if discriminator != &requested => { + let provider = ForceLayout::new(graphs); + let layout = provider.init(); + + Self::InProgress { + discriminator: requested, + layout, + provider, + } + } + // We keep iterating on the layout until it is stable. + Self::InProgress { + discriminator, + mut layout, + mut provider, + } => match provider.tick(&mut layout) { + true => Self::Finished { + discriminator, + layout, + _provider: provider, + }, + false => Self::InProgress { + discriminator, + layout, + provider, + }, + }, + } + } + + /// This method is lazy. A new layout is only computed if the current timestamp requires it. + pub fn get<'a>( + &'a mut self, + hash: Discriminator, + graphs: impl Iterator> + Clone, + ) -> &'a mut Layout { + *self = std::mem::take(self).update(hash, graphs); + + match self { + Self::Finished { layout, .. } | Self::InProgress { layout, .. } => layout, + Self::None => unreachable!(), // We just set the state to `Self::Current` above. + } + } +} diff --git a/crates/viewer/re_space_view_graph/src/view.rs b/crates/viewer/re_space_view_graph/src/view.rs new file mode 100644 index 000000000000..e0d5362a0209 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -0,0 +1,272 @@ +use std::hash::{Hash as _, Hasher as _}; + +use re_log_types::EntityPath; +use re_space_view::{ + controls::{DRAG_PAN2D_BUTTON, ZOOM_SCROLL_MODIFIER}, + view_property_ui, +}; +use re_types::{ + blueprint::{self, archetypes::VisualBounds2D}, + components, SpaceViewClassIdentifier, +}; +use re_ui::{self, ModifiersMarkdown, MouseButtonMarkdown, UiExt as _}; +use re_viewer_context::{ + external::re_entity_db::InstancePath, IdentifiedViewSystem as _, Item, RecommendedSpaceView, + SpaceViewClass, SpaceViewClassLayoutPriority, SpaceViewClassRegistryError, SpaceViewId, + SpaceViewSpawnHeuristics, SpaceViewState, SpaceViewStateExt as _, + SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, SystemExecutionOutput, ViewQuery, + ViewerContext, +}; +use re_viewport_blueprint::ViewProperty; + +use crate::{ + graph::Graph, + ui::{scene::SceneBuilder, Discriminator, GraphSpaceViewState}, + visualizers::{merge, EdgesVisualizer, NodeVisualizer}, +}; + +#[derive(Default)] +pub struct GraphSpaceView; + +impl SpaceViewClass for GraphSpaceView { + // State type as described above. + + fn identifier() -> SpaceViewClassIdentifier { + "Graph".into() + } + + fn display_name(&self) -> &'static str { + "Graph" + } + + fn icon(&self) -> &'static re_ui::Icon { + &re_ui::icons::SPACE_VIEW_GRAPH + } + + fn help_markdown(&self, egui_ctx: &egui::Context) -> String { + format!( + r"# Graph View + +Display a graph of nodes and edges. + +## Navigation controls +- Pinch gesture or {zoom_scroll_modifier} + scroll to zoom. +- Click and drag with the {drag_pan2d_button} to pan. +- Double-click to reset the view.", + zoom_scroll_modifier = ModifiersMarkdown(ZOOM_SCROLL_MODIFIER, egui_ctx), + drag_pan2d_button = MouseButtonMarkdown(DRAG_PAN2D_BUTTON), + ) + } + + /// Register all systems (contexts & parts) that the space view needs. + fn on_register( + &self, + system_registry: &mut SpaceViewSystemRegistrator<'_>, + ) -> Result<(), SpaceViewClassRegistryError> { + system_registry.register_visualizer::()?; + system_registry.register_visualizer::() + } + + fn new_state(&self) -> Box { + Box::::default() + } + + fn preferred_tile_aspect_ratio(&self, state: &dyn SpaceViewState) -> Option { + let state = state.downcast_ref::().ok()?; + + if let Some(bounds) = state.world_bounds { + let width = bounds.x_range.abs_len() as f32; + let height = bounds.y_range.abs_len() as f32; + return Some(width / height); + } + + if let Some(rect) = state.layout_state.bounding_rect() { + let width = rect.width().abs(); + let height = rect.height().abs(); + return Some(width / height); + } + None + } + + fn layout_priority(&self) -> SpaceViewClassLayoutPriority { + Default::default() + } + + fn spawn_heuristics(&self, ctx: &ViewerContext<'_>) -> SpaceViewSpawnHeuristics { + if let Some(applicable) = ctx + .applicable_entities_per_visualizer + .get(&NodeVisualizer::identifier()) + { + SpaceViewSpawnHeuristics::new( + applicable + .iter() + .cloned() + .map(RecommendedSpaceView::new_single_entity), + ) + } else { + SpaceViewSpawnHeuristics::empty() + } + } + + /// Additional UI displayed when the space view is selected. + /// + /// In this sample we show a combo box to select the color coordinates mode. + fn selection_ui( + &self, + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + state: &mut dyn SpaceViewState, + _space_origin: &EntityPath, + space_view_id: SpaceViewId, + ) -> Result<(), SpaceViewSystemExecutionError> { + let state = state.downcast_mut::()?; + + ui.selection_grid("graph_view_settings_ui").show(ui, |ui| { + state.layout_ui(ui); + state.simulation_ui(ui); + state.debug_ui(ui); + }); + + view_property_ui::(ctx, ui, space_view_id, self, state); + + Ok(()) + } + + /// The contents of the Space View window and all interaction within it. + /// + /// This is called with freshly created & executed context & part systems. + fn ui( + &self, + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + state: &mut dyn SpaceViewState, + query: &ViewQuery<'_>, + system_output: SystemExecutionOutput, + ) -> Result<(), SpaceViewSystemExecutionError> { + let node_data = &system_output.view_systems.get::()?.data; + let edge_data = &system_output.view_systems.get::()?.data; + + let graphs = merge(node_data, edge_data) + .map(|(ent, nodes, edges)| (ent, Graph::new(nodes, edges))) + .collect::>(); + + // We could move this computation to the visualizers to improve + // performance if needed. + let discriminator = { + let mut hasher = ahash::AHasher::default(); + graphs.hash(&mut hasher); + Discriminator::new(hasher.finish()) + }; + + let state = state.downcast_mut::()?; + + let bounds_property = ViewProperty::from_archetype::( + ctx.blueprint_db(), + ctx.blueprint_query, + query.space_view_id, + ); + + let bounds: blueprint::components::VisualBounds2D = + bounds_property.component_or_fallback(ctx, self, state)?; + + let layout_was_empty = state.layout_state.is_none(); + let layout = state + .layout_state + .get(discriminator, graphs.iter().map(|(_, graph)| graph)); + + let mut needs_remeasure = false; + + state.world_bounds = Some(bounds); + let bounds_rect: egui::Rect = bounds.into(); + + let mut scene_builder = SceneBuilder::from_world_bounds(bounds_rect); + + if state.show_debug { + scene_builder.show_debug(); + } + + let (new_world_bounds, response) = scene_builder.add(ui, |mut scene| { + for (entity, graph) in &graphs { + // We use the following to keep track of the bounding box over nodes in an entity. + let mut entity_rect = egui::Rect::NOTHING; + + let ent_highlights = query.highlights.entity_highlight((*entity).hash()); + + // Draw explicit nodes. + for node in graph.nodes_explicit() { + let pos = layout.get(&node.index).unwrap_or(egui::Rect::ZERO); + + let response = scene.explicit_node( + pos.min, + node, + ent_highlights.index_highlight(node.instance), + ); + + if response.clicked() { + let instance_path = + InstancePath::instance((*entity).clone(), node.instance); + ctx.select_hovered_on_click( + &response, + vec![(Item::DataResult(query.space_view_id, instance_path), None)] + .into_iter(), + ); + } + + entity_rect = entity_rect.union(response.rect); + needs_remeasure |= layout.update(&node.index, response.rect); + } + + // Draw implicit nodes. + for node in graph.nodes_implicit() { + let current = layout.get(&node.index).unwrap_or(egui::Rect::ZERO); + let response = scene.implicit_node(current.min, node); + entity_rect = entity_rect.union(response.rect); + needs_remeasure |= layout.update(&node.index, response.rect); + } + + // Draw edges. + for edge in graph.edges() { + if let (Some(from), Some(to)) = ( + layout.get(&edge.source_index), + layout.get(&edge.target_index), + ) { + let show_arrow = graph.kind() == components::GraphType::Directed; + scene.edge(from, to, edge, show_arrow); + } + } + + // Draw entity rect. + if graphs.len() > 1 && entity_rect.is_positive() { + let response = scene.entity(entity, entity_rect, &query.highlights); + + let instance_path = InstancePath::entity_all((*entity).clone()); + ctx.select_hovered_on_click( + &response, + vec![(Item::DataResult(query.space_view_id, instance_path), None)] + .into_iter(), + ); + } + } + }); + + // Update blueprint if changed + let updated_bounds: blueprint::components::VisualBounds2D = new_world_bounds.into(); + if response.double_clicked() || layout_was_empty { + bounds_property.reset_blueprint_component::(ctx); + } else if bounds != updated_bounds { + bounds_property.save_blueprint_component(ctx, &updated_bounds); + } + // Update stored bounds on the state, so visualizers see an up-to-date value. + state.world_bounds = Some(bounds); + + if needs_remeasure { + ui.ctx().request_discard("layout needed a remeasure"); + } + + if state.layout_state.is_in_progress() { + ui.ctx().request_repaint(); + } + + Ok(()) + } +} diff --git a/crates/viewer/re_space_view_graph/src/visualizers/edges.rs b/crates/viewer/re_space_view_graph/src/visualizers/edges.rs new file mode 100644 index 000000000000..e291cd193be6 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/visualizers/edges.rs @@ -0,0 +1,122 @@ +use re_chunk::LatestAtQuery; +use re_log_types::EntityPath; +use re_space_view::{DataResultQuery, RangeResultsExt}; +use re_types::{ + self, archetypes, + components::{self, GraphEdge, GraphNode}, + Component as _, +}; +use re_viewer_context::{ + self, IdentifiedViewSystem, SpaceViewSystemExecutionError, ViewContext, ViewContextCollection, + ViewQuery, ViewSystemIdentifier, VisualizerQueryInfo, VisualizerSystem, +}; + +use crate::graph::NodeIndex; + +#[derive(Default)] +pub struct EdgesVisualizer { + pub data: ahash::HashMap, +} + +pub struct EdgeInstance { + pub source: GraphNode, + pub target: GraphNode, + pub source_index: NodeIndex, + pub target_index: NodeIndex, +} + +impl std::hash::Hash for EdgeInstance { + fn hash(&self, state: &mut H) { + // We use the more verbose destructring here, to make sure that we + // exhaustively consider all fields when hashing (we get a compiler + // warning when we forget a field). + let Self { + // The index fields already uniquely identify `source` and `target`. + source: _, + target: _, + source_index, + target_index, + } = self; + source_index.hash(state); + target_index.hash(state); + } +} + +#[derive(Hash)] +pub struct EdgeData { + pub graph_type: components::GraphType, + pub edges: Vec, +} + +impl IdentifiedViewSystem for EdgesVisualizer { + fn identifier() -> ViewSystemIdentifier { + "GraphEdges".into() + } +} + +impl VisualizerSystem for EdgesVisualizer { + fn visualizer_query_info(&self) -> VisualizerQueryInfo { + VisualizerQueryInfo::from_archetype::() + } + + /// Populates the scene part with data from the store. + fn execute( + &mut self, + ctx: &ViewContext<'_>, + query: &ViewQuery<'_>, + _context_systems: &ViewContextCollection, + ) -> Result, SpaceViewSystemExecutionError> { + let timeline_query = LatestAtQuery::new(query.timeline, query.latest_at); + + for data_result in query.iter_visible_data_results(ctx, Self::identifier()) { + let results = data_result + .latest_at_with_blueprint_resolved_data::( + ctx, + &timeline_query, + ); + + let all_indexed_edges = results.iter_as(query.timeline, components::GraphEdge::name()); + let graph_type = results.get_mono_with_fallback::(); + + for (_index, edges) in all_indexed_edges.component::() { + let edges = edges + .iter() + .map(|edge| { + let source = GraphNode::from(edge.first.clone()); + let target = GraphNode::from(edge.second.clone()); + + let entity_path = &data_result.entity_path; + let source_index = NodeIndex::from_entity_node(entity_path, &source); + let target_index = NodeIndex::from_entity_node(entity_path, &target); + + EdgeInstance { + source, + target, + source_index, + target_index, + } + }) + .collect(); + + self.data.insert( + data_result.entity_path.clone(), + EdgeData { edges, graph_type }, + ); + } + } + + // We're not using `re_renderer` here, so return an empty vector. + // If you want to draw additional primitives here, you can emit re_renderer draw data here directly. + Ok(Vec::new()) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn fallback_provider(&self) -> &dyn re_viewer_context::ComponentFallbackProvider { + self + } +} + +re_viewer_context::impl_component_fallback_provider!(EdgesVisualizer => []); diff --git a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs new file mode 100644 index 000000000000..0f91ee66492e --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs @@ -0,0 +1,27 @@ +mod edges; +mod nodes; + +use std::collections::BTreeSet; + +pub use edges::{EdgeData, EdgeInstance, EdgesVisualizer}; +pub use nodes::{NodeData, NodeInstance, NodeVisualizer}; + +use re_chunk::EntityPath; + +/// Iterates over all entities and joins the node and edge data. +pub fn merge<'a>( + node_data: &'a ahash::HashMap, + edge_data: &'a ahash::HashMap, +) -> impl Iterator, Option<&'a EdgeData>)> + 'a { + // We sort the entities to ensure that we always process them in the same order. + let unique_entities = node_data + .keys() + .chain(edge_data.keys()) + .collect::>(); + + unique_entities.into_iter().map(|entity| { + let nodes = node_data.get(entity); + let edges = edge_data.get(entity); + (entity, nodes, edges) + }) +} diff --git a/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs b/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs new file mode 100644 index 000000000000..2c5896b94610 --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs @@ -0,0 +1,173 @@ +use re_chunk::LatestAtQuery; +use re_log_types::{EntityPath, Instance}; +use re_query::{clamped_zip_2x4, range_zip_1x4}; +use re_space_view::{DataResultQuery, RangeResultsExt}; +use re_types::components::{Color, Radius, ShowLabels}; +use re_types::datatypes::Float32; +use re_types::{ + self, archetypes, + components::{self}, + ArrowString, Component as _, +}; +use re_viewer_context::{ + self, IdentifiedViewSystem, QueryContext, SpaceViewSystemExecutionError, + TypedComponentFallbackProvider, ViewContext, ViewContextCollection, ViewQuery, + ViewSystemIdentifier, VisualizerQueryInfo, VisualizerSystem, +}; + +use crate::graph::NodeIndex; + +#[derive(Default)] +pub struct NodeVisualizer { + pub data: ahash::HashMap, +} + +pub struct NodeInstance { + pub node: components::GraphNode, + pub instance: Instance, + pub index: NodeIndex, + pub label: Option, + pub show_label: bool, + pub color: Option, + pub position: Option, + pub radius: Option, +} + +impl std::hash::Hash for NodeInstance { + fn hash(&self, state: &mut H) { + // We use the more verbose destructring here, to make sure that we + // exhaustively consider all fields when hashing (we get a compiler + // warning when we forget a field). + let Self { + // The index already uniquely identifies a node, so we don't need to + // hash the node itself. + node: _, + instance, + index, + label, + show_label, + color, + position, + radius, + } = self; + instance.hash(state); + index.hash(state); + label.hash(state); + show_label.hash(state); + color.hash(state); + // The following fields don't implement `Hash`. + position.as_ref().map(bytemuck::bytes_of).hash(state); + radius.as_ref().map(bytemuck::bytes_of).hash(state); + } +} + +#[derive(Hash)] +pub struct NodeData { + pub nodes: Vec, +} + +impl IdentifiedViewSystem for NodeVisualizer { + fn identifier() -> ViewSystemIdentifier { + "GraphNodes".into() + } +} + +impl VisualizerSystem for NodeVisualizer { + fn visualizer_query_info(&self) -> VisualizerQueryInfo { + VisualizerQueryInfo::from_archetype::() + } + + /// Populates the scene part with data from the store. + fn execute( + &mut self, + ctx: &ViewContext<'_>, + query: &ViewQuery<'_>, + _context_systems: &ViewContextCollection, + ) -> Result, SpaceViewSystemExecutionError> { + let timeline_query = LatestAtQuery::new(query.timeline, query.latest_at); + + for data_result in query.iter_visible_data_results(ctx, Self::identifier()) { + let results = data_result + .latest_at_with_blueprint_resolved_data::( + ctx, + &timeline_query, + ); + + let all_indexed_nodes = results.iter_as(query.timeline, components::GraphNode::name()); + let all_colors = results.iter_as(query.timeline, components::Color::name()); + let all_positions = results.iter_as(query.timeline, components::Position2D::name()); + let all_labels = results.iter_as(query.timeline, components::Text::name()); + let all_radii = results.iter_as(query.timeline, components::Radius::name()); + let show_label = results + .get_mono::() + .map_or(true, bool::from); + + let data = range_zip_1x4( + all_indexed_nodes.component::(), + all_colors.component::(), + all_positions.primitive_array::<2, f32>(), + all_labels.string(), + all_radii.component::(), + ); + + for (_index, nodes, colors, positions, labels, radii) in data { + let nodes = clamped_zip_2x4( + nodes.iter(), + (0..).map(Instance::from), + colors.unwrap_or_default().iter().map(Option::Some), + Option::<&Color>::default, + positions + .unwrap_or_default() + .iter() + .copied() + .map(Option::Some), + Option::<[f32; 2]>::default, + labels.unwrap_or_default().iter().map(Option::Some), + Option::<&ArrowString>::default, + radii.unwrap_or_default().iter().map(Option::Some), + Option::<&components::Radius>::default, + ) + .map( + |(node, instance, color, position, label, radius)| NodeInstance { + node: node.clone(), + instance, + index: NodeIndex::from_entity_node(&data_result.entity_path, node), + color: color.map(|&Color(color)| color.into()), + position: position.map(|[x, y]| egui::Pos2::new(x, y)), + label: label.cloned(), + show_label, + radius: radius.copied(), + }, + ) + .collect::>(); + + self.data + .insert(data_result.entity_path.clone(), NodeData { nodes }); + } + } + + Ok(Vec::new()) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn fallback_provider(&self) -> &dyn re_viewer_context::ComponentFallbackProvider { + self + } +} + +impl TypedComponentFallbackProvider for NodeVisualizer { + fn fallback_for(&self, _ctx: &QueryContext<'_>) -> ShowLabels { + true.into() + } +} + +impl TypedComponentFallbackProvider for NodeVisualizer { + fn fallback_for(&self, _ctx: &QueryContext<'_>) -> Radius { + Radius(Float32(4.0f32)) + } +} + +re_viewer_context::impl_component_fallback_provider!(NodeVisualizer => [ShowLabels, Radius]); diff --git a/crates/viewer/re_ui/data/icons/spaceview_graph.png b/crates/viewer/re_ui/data/icons/spaceview_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..9b709e7c508f58c76db69bded8d8ccbf536614dd GIT binary patch literal 462 zcmV;<0WtoGP)fm%qnC$U@YkFtX2_Yq;Dk7DRGG#uar-od*~0bqhjNau22>d12R{3kA9$zXcxL zQP0#XrqL#$&oHB&xW$F+icn%SdQTcd zX6KQ^vH#s4*i?n#?<7rZMOq`C0udEyjG-in^~`68s7PZBUnu#eRhV%cdVY){S8(F+ zkitwD6~pVJBDilV!)(I}}p8x;=07*qoM6N<$ Ef{A3kWB>pF literal 0 HcmV?d00001 diff --git a/crates/viewer/re_ui/src/icons.rs b/crates/viewer/re_ui/src/icons.rs index 01ac9c8de55b..7b5c2770f5c7 100644 --- a/crates/viewer/re_ui/src/icons.rs +++ b/crates/viewer/re_ui/src/icons.rs @@ -90,6 +90,7 @@ pub const CONTAINER_VERTICAL: Icon = icon_from_path!("../data/icons/container_ve pub const SPACE_VIEW_2D: Icon = icon_from_path!("../data/icons/spaceview_2d.png"); pub const SPACE_VIEW_3D: Icon = icon_from_path!("../data/icons/spaceview_3d.png"); pub const SPACE_VIEW_DATAFRAME: Icon = icon_from_path!("../data/icons/spaceview_dataframe.png"); +pub const SPACE_VIEW_GRAPH: Icon = icon_from_path!("../data/icons/spaceview_graph.png"); pub const SPACE_VIEW_GENERIC: Icon = icon_from_path!("../data/icons/spaceview_generic.png"); pub const SPACE_VIEW_HISTOGRAM: Icon = icon_from_path!("../data/icons/spaceview_histogram.png"); pub const SPACE_VIEW_LOG: Icon = icon_from_path!("../data/icons/spaceview_log.png"); diff --git a/crates/viewer/re_viewer/Cargo.toml b/crates/viewer/re_viewer/Cargo.toml index 9e9f10fa5253..c6783b229fb0 100644 --- a/crates/viewer/re_viewer/Cargo.toml +++ b/crates/viewer/re_viewer/Cargo.toml @@ -73,6 +73,7 @@ re_sdk_comms.workspace = true re_smart_channel.workspace = true re_space_view_bar_chart.workspace = true re_space_view_dataframe.workspace = true +re_space_view_graph.workspace = true re_space_view_spatial.workspace = true re_space_view_tensor.workspace = true re_space_view_text_document.workspace = true diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 54f5d937feba..086d5db78154 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -1891,6 +1891,8 @@ fn populate_space_view_class_registry_with_builtin( ) -> Result<(), SpaceViewClassRegistryError> { re_tracing::profile_function!(); space_view_class_registry.add_class::()?; + space_view_class_registry.add_class::()?; + space_view_class_registry.add_class::()?; #[cfg(feature = "map_view")] space_view_class_registry.add_class::()?; space_view_class_registry.add_class::()?; @@ -1899,7 +1901,6 @@ fn populate_space_view_class_registry_with_builtin( space_view_class_registry.add_class::()?; space_view_class_registry.add_class::()?; space_view_class_registry.add_class::()?; - space_view_class_registry.add_class::()?; Ok(()) } diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index 0eea1468ccb8..d8ac61f325a1 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -448,6 +448,30 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "An edge in a graph connecting two nodes.", + custom_placeholder: Some(GraphEdge::default().to_arrow2()?), + datatype: GraphEdge::arrow2_datatype(), + }, + ), + ( + ::name(), + ComponentReflection { + docstring_md: "A string-based ID representing a node in a graph.", + custom_placeholder: Some(GraphNode::default().to_arrow2()?), + datatype: GraphNode::arrow2_datatype(), + }, + ), + ( + ::name(), + ComponentReflection { + docstring_md: "Specifies if a graph has directed or undirected edges.", + custom_placeholder: Some(GraphType::default().to_arrow2()?), + datatype: GraphType::arrow2_datatype(), + }, + ), ( ::name(), ComponentReflection { @@ -1305,6 +1329,58 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ], }, ), + ( + ArchetypeName::new("rerun.archetypes.GraphEdges"), + ArchetypeReflection { + display_name: "Graph edges", + view_types: &["GraphView"], + fields: vec![ + ArchetypeFieldReflection { component_name : + "rerun.components.GraphEdge".into(), display_name : "Edges", + docstring_md : + "A list of node tuples.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : true, }, ArchetypeFieldReflection { component_name : + "rerun.components.GraphType".into(), display_name : "Graph type", + docstring_md : + "Specifies if the graph is directed or undirected.\n\nIf no `GraphType` is provided, the graph is assumed to be undirected.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : false, }, + ], + }, + ), + ( + ArchetypeName::new("rerun.archetypes.GraphNodes"), + ArchetypeReflection { + display_name: "Graph nodes", + view_types: &["GraphView"], + fields: vec![ + ArchetypeFieldReflection { component_name : + "rerun.components.GraphNode".into(), display_name : "Node ids", + docstring_md : + "A list of node IDs.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : true, }, ArchetypeFieldReflection { component_name : + "rerun.components.Position2D".into(), display_name : "Positions", + docstring_md : + "Optional center positions of the nodes.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : false, }, ArchetypeFieldReflection { component_name : + "rerun.components.Color".into(), display_name : "Colors", + docstring_md : + "Optional colors for the boxes.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : false, }, ArchetypeFieldReflection { component_name : + "rerun.components.Text".into(), display_name : "Labels", docstring_md + : + "Optional text labels for the node.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : false, }, ArchetypeFieldReflection { component_name : + "rerun.components.ShowLabels".into(), display_name : "Show labels", + docstring_md : + "Optional choice of whether the text labels should be shown by default.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : false, }, ArchetypeFieldReflection { component_name : + "rerun.components.Radius".into(), display_name : "Radii", + docstring_md : + "Optional radii for nodes.\n\n⚠\u{fe0f} **This type is experimental and may be removed in future versions**", + is_required : false, }, + ], + }, + ), ( ArchetypeName::new("rerun.archetypes.Image"), ArchetypeReflection { diff --git a/docs/content/reference/types/archetypes.md b/docs/content/reference/types/archetypes.md index 65e30ca822b4..20ecbf830c6a 100644 --- a/docs/content/reference/types/archetypes.md +++ b/docs/content/reference/types/archetypes.md @@ -17,6 +17,11 @@ This page lists all built-in archetypes. * [`GeoLineStrings`](archetypes/geo_line_strings.md): Geospatial line strings with positions expressed in [EPSG:4326](https://epsg.io/4326) altitude and longitude (North/East-positive degrees), and optional colors and radii. * [`GeoPoints`](archetypes/geo_points.md): Geospatial points with positions expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees), and optional colors and radii. +## Graph + +* [`GraphEdges`](archetypes/graph_edges.md): A list of edges in a graph. +* [`GraphNodes`](archetypes/graph_nodes.md): A list of nodes in a graph with optional labels, colors, etc. + ## Image & tensor * [`DepthImage`](archetypes/depth_image.md): A depth image, i.e. as captured by a depth camera. diff --git a/docs/content/reference/types/archetypes/.gitattributes b/docs/content/reference/types/archetypes/.gitattributes index 65b162a42607..962c2cdd2d55 100644 --- a/docs/content/reference/types/archetypes/.gitattributes +++ b/docs/content/reference/types/archetypes/.gitattributes @@ -17,6 +17,8 @@ ellipsoids3d.md linguist-generated=true encoded_image.md linguist-generated=true geo_line_strings.md linguist-generated=true geo_points.md linguist-generated=true +graph_edges.md linguist-generated=true +graph_nodes.md linguist-generated=true image.md linguist-generated=true instance_poses3d.md linguist-generated=true line_strips2d.md linguist-generated=true diff --git a/docs/content/reference/types/archetypes/graph_edges.md b/docs/content/reference/types/archetypes/graph_edges.md new file mode 100644 index 000000000000..626954039656 --- /dev/null +++ b/docs/content/reference/types/archetypes/graph_edges.md @@ -0,0 +1,41 @@ +--- +title: "GraphEdges" +--- + + + +⚠️ **This is an experimental API! It is not fully supported, and is likely to change significantly in future versions.** + +A list of edges in a graph. + +By default, edges are undirected. + +## Components + +**Required**: [`GraphEdge`](../components/graph_edge.md) + +**Recommended**: [`GraphType`](../components/graph_type.md) + +## Shown in +* [GraphView](../views/graph_view.md) +* [DataframeView](../views/dataframe_view.md) + +## API reference links + * 🌊 [C++ API docs for `GraphEdges`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1archetypes_1_1GraphEdges.html?speculative-link) + * 🐍 [Python API docs for `GraphEdges`](https://ref.rerun.io/docs/python/stable/common/archetypes?speculative-link#rerun.archetypes.GraphEdges) + * 🦀 [Rust API docs for `GraphEdges`](https://docs.rs/rerun/latest/rerun/archetypes/struct.GraphEdges.html?speculative-link) + +## Examples + +### Simple undirected graph + +snippet: archetypes/graph_undirected + + + +### Simple directed graph + +snippet: archetypes/graph_directed + + + diff --git a/docs/content/reference/types/archetypes/graph_nodes.md b/docs/content/reference/types/archetypes/graph_nodes.md new file mode 100644 index 000000000000..694708eaa5be --- /dev/null +++ b/docs/content/reference/types/archetypes/graph_nodes.md @@ -0,0 +1,39 @@ +--- +title: "GraphNodes" +--- + + + +⚠️ **This is an experimental API! It is not fully supported, and is likely to change significantly in future versions.** + +A list of nodes in a graph with optional labels, colors, etc. + +## Components + +**Required**: [`GraphNode`](../components/graph_node.md) + +**Optional**: [`Position2D`](../components/position2d.md), [`Color`](../components/color.md), [`Text`](../components/text.md), [`ShowLabels`](../components/show_labels.md), [`Radius`](../components/radius.md) + +## Shown in +* [GraphView](../views/graph_view.md) +* [DataframeView](../views/dataframe_view.md) + +## API reference links + * 🌊 [C++ API docs for `GraphNodes`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1archetypes_1_1GraphNodes.html?speculative-link) + * 🐍 [Python API docs for `GraphNodes`](https://ref.rerun.io/docs/python/stable/common/archetypes?speculative-link#rerun.archetypes.GraphNodes) + * 🦀 [Rust API docs for `GraphNodes`](https://docs.rs/rerun/latest/rerun/archetypes/struct.GraphNodes.html?speculative-link) + +## Examples + +### Simple undirected graph + +snippet: archetypes/graph_undirected + + + +### Simple directed graph + +snippet: archetypes/graph_directed + + + diff --git a/docs/content/reference/types/components.md b/docs/content/reference/types/components.md index 9e0e3f07f681..e95094cca0c5 100644 --- a/docs/content/reference/types/components.md +++ b/docs/content/reference/types/components.md @@ -30,6 +30,9 @@ on [Entities and Components](../../concepts/entity-component.md). * [`FillRatio`](components/fill_ratio.md): How much a primitive fills out the available space. * [`GammaCorrection`](components/gamma_correction.md): A gamma correction value to be used with a scalar value or color. * [`GeoLineString`](components/geo_line_string.md): A geospatial line string expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees). +* [`GraphEdge`](components/graph_edge.md): An edge in a graph connecting two nodes. +* [`GraphNode`](components/graph_node.md): A string-based ID representing a node in a graph. +* [`GraphType`](components/graph_type.md): Specifies if a graph has directed or undirected edges. * [`HalfSize2D`](components/half_size2d.md): Half-size (radius) of a 2D box. * [`HalfSize3D`](components/half_size3d.md): Half-size (radius) of a 3D box. * [`ImageBuffer`](components/image_buffer.md): A buffer that is known to store image data. diff --git a/docs/content/reference/types/components/.gitattributes b/docs/content/reference/types/components/.gitattributes index d2b7740214d4..7d03d1206dc9 100644 --- a/docs/content/reference/types/components/.gitattributes +++ b/docs/content/reference/types/components/.gitattributes @@ -18,6 +18,9 @@ fill_mode.md linguist-generated=true fill_ratio.md linguist-generated=true gamma_correction.md linguist-generated=true geo_line_string.md linguist-generated=true +graph_edge.md linguist-generated=true +graph_node.md linguist-generated=true +graph_type.md linguist-generated=true half_size2d.md linguist-generated=true half_size3d.md linguist-generated=true image_buffer.md linguist-generated=true diff --git a/docs/content/reference/types/components/color.md b/docs/content/reference/types/components/color.md index 32fcd7eb4c97..6babca0c5743 100644 --- a/docs/content/reference/types/components/color.md +++ b/docs/content/reference/types/components/color.md @@ -34,6 +34,7 @@ uint32 * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GeoLineStrings`](../archetypes/geo_line_strings.md) * [`GeoPoints`](../archetypes/geo_points.md) +* [`GraphNodes`](../archetypes/graph_nodes.md?speculative-link) * [`LineStrips2D`](../archetypes/line_strips2d.md) * [`LineStrips3D`](../archetypes/line_strips3d.md) * [`Mesh3D`](../archetypes/mesh3d.md) diff --git a/docs/content/reference/types/components/graph_edge.md b/docs/content/reference/types/components/graph_edge.md new file mode 100644 index 000000000000..572c0f45abbe --- /dev/null +++ b/docs/content/reference/types/components/graph_edge.md @@ -0,0 +1,28 @@ +--- +title: "GraphEdge" +--- + + +An edge in a graph connecting two nodes. + +## Rerun datatype +[`Utf8Pair`](../datatypes/utf8pair.md) + + +## Arrow datatype +``` +Struct { + first: utf8 + second: utf8 +} +``` + +## API reference links + * 🌊 [C++ API docs for `GraphEdge`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1GraphEdge.html?speculative-link) + * 🐍 [Python API docs for `GraphEdge`](https://ref.rerun.io/docs/python/stable/common/components?speculative-link#rerun.components.GraphEdge) + * 🦀 [Rust API docs for `GraphEdge`](https://docs.rs/rerun/latest/rerun/components/struct.GraphEdge.html?speculative-link) + + +## Used by + +* [`GraphEdges`](../archetypes/graph_edges.md?speculative-link) diff --git a/docs/content/reference/types/components/graph_node.md b/docs/content/reference/types/components/graph_node.md new file mode 100644 index 000000000000..63339a42164e --- /dev/null +++ b/docs/content/reference/types/components/graph_node.md @@ -0,0 +1,25 @@ +--- +title: "GraphNode" +--- + + +A string-based ID representing a node in a graph. + +## Rerun datatype +[`Utf8`](../datatypes/utf8.md) + + +## Arrow datatype +``` +utf8 +``` + +## API reference links + * 🌊 [C++ API docs for `GraphNode`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1GraphNode.html?speculative-link) + * 🐍 [Python API docs for `GraphNode`](https://ref.rerun.io/docs/python/stable/common/components?speculative-link#rerun.components.GraphNode) + * 🦀 [Rust API docs for `GraphNode`](https://docs.rs/rerun/latest/rerun/components/struct.GraphNode.html?speculative-link) + + +## Used by + +* [`GraphNodes`](../archetypes/graph_nodes.md?speculative-link) diff --git a/docs/content/reference/types/components/graph_type.md b/docs/content/reference/types/components/graph_type.md new file mode 100644 index 000000000000..30a9869f4431 --- /dev/null +++ b/docs/content/reference/types/components/graph_type.md @@ -0,0 +1,29 @@ +--- +title: "GraphType" +--- + + +Specifies if a graph has directed or undirected edges. + +## Variants +#### `Undirected` = 1 +The graph has undirected edges. + +#### `Directed` = 2 +The graph has directed edges. + + +## Arrow datatype +``` +uint8 +``` + +## API reference links + * 🌊 [C++ API docs for `GraphType`](https://ref.rerun.io/docs/cpp/stable/namespacererun_1_1components.html?speculative-link) + * 🐍 [Python API docs for `GraphType`](https://ref.rerun.io/docs/python/stable/common/components?speculative-link#rerun.components.GraphType) + * 🦀 [Rust API docs for `GraphType`](https://docs.rs/rerun/latest/rerun/components/enum.GraphType.html?speculative-link) + + +## Used by + +* [`GraphEdges`](../archetypes/graph_edges.md?speculative-link) diff --git a/docs/content/reference/types/components/position2d.md b/docs/content/reference/types/components/position2d.md index 783dc3757ad3..3c0db7b55692 100644 --- a/docs/content/reference/types/components/position2d.md +++ b/docs/content/reference/types/components/position2d.md @@ -24,4 +24,5 @@ FixedSizeList<2, float32> * [`Arrows2D`](../archetypes/arrows2d.md) * [`Boxes2D`](../archetypes/boxes2d.md) +* [`GraphNodes`](../archetypes/graph_nodes.md?speculative-link) * [`Points2D`](../archetypes/points2d.md) diff --git a/docs/content/reference/types/components/radius.md b/docs/content/reference/types/components/radius.md index 5db6e4081e91..f12477593b58 100644 --- a/docs/content/reference/types/components/radius.md +++ b/docs/content/reference/types/components/radius.md @@ -37,6 +37,7 @@ float32 * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) * [`GeoLineStrings`](../archetypes/geo_line_strings.md) * [`GeoPoints`](../archetypes/geo_points.md) +* [`GraphNodes`](../archetypes/graph_nodes.md?speculative-link) * [`LineStrips2D`](../archetypes/line_strips2d.md) * [`LineStrips3D`](../archetypes/line_strips3d.md) * [`Points2D`](../archetypes/points2d.md) diff --git a/docs/content/reference/types/components/show_labels.md b/docs/content/reference/types/components/show_labels.md index b8b6bc9e0fa7..8ed7d1d008f3 100644 --- a/docs/content/reference/types/components/show_labels.md +++ b/docs/content/reference/types/components/show_labels.md @@ -32,6 +32,7 @@ boolean * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) +* [`GraphNodes`](../archetypes/graph_nodes.md?speculative-link) * [`LineStrips2D`](../archetypes/line_strips2d.md) * [`LineStrips3D`](../archetypes/line_strips3d.md) * [`Points2D`](../archetypes/points2d.md) diff --git a/docs/content/reference/types/components/text.md b/docs/content/reference/types/components/text.md index 1dcf5e2284cb..b86cf1f6c29a 100644 --- a/docs/content/reference/types/components/text.md +++ b/docs/content/reference/types/components/text.md @@ -28,6 +28,7 @@ utf8 * [`Boxes3D`](../archetypes/boxes3d.md) * [`Capsules3D`](../archetypes/capsules3d.md) * [`Ellipsoids3D`](../archetypes/ellipsoids3d.md) +* [`GraphNodes`](../archetypes/graph_nodes.md?speculative-link) * [`LineStrips2D`](../archetypes/line_strips2d.md) * [`LineStrips3D`](../archetypes/line_strips3d.md) * [`Points2D`](../archetypes/points2d.md) diff --git a/docs/content/reference/types/datatypes.md b/docs/content/reference/types/datatypes.md index bb33cebb6c34..653d0db2c963 100644 --- a/docs/content/reference/types/datatypes.md +++ b/docs/content/reference/types/datatypes.md @@ -46,6 +46,7 @@ Data types are the lowest layer of the data model hierarchy. They are re-usable * [`UVec3D`](datatypes/uvec3d.md): A uint32 vector in 3D space. * [`UVec4D`](datatypes/uvec4d.md): A uint vector in 4D space. * [`Utf8`](datatypes/utf8.md): A string of text, encoded as UTF-8. +* [`Utf8Pair`](datatypes/utf8pair.md): Stores a tuple of UTF-8 strings. * [`Uuid`](datatypes/uuid.md): A 16-byte UUID. * [`Vec2D`](datatypes/vec2d.md): A vector in 2D space. * [`Vec3D`](datatypes/vec3d.md): A vector in 3D space. diff --git a/docs/content/reference/types/datatypes/.gitattributes b/docs/content/reference/types/datatypes/.gitattributes index 408958b8fb1c..16fee72d8210 100644 --- a/docs/content/reference/types/datatypes/.gitattributes +++ b/docs/content/reference/types/datatypes/.gitattributes @@ -37,6 +37,7 @@ uint16.md linguist-generated=true uint32.md linguist-generated=true uint64.md linguist-generated=true utf8.md linguist-generated=true +utf8pair.md linguist-generated=true uuid.md linguist-generated=true uvec2d.md linguist-generated=true uvec3d.md linguist-generated=true diff --git a/docs/content/reference/types/datatypes/utf8.md b/docs/content/reference/types/datatypes/utf8.md index 7da350ef1499..1c8ec70eca28 100644 --- a/docs/content/reference/types/datatypes/utf8.md +++ b/docs/content/reference/types/datatypes/utf8.md @@ -20,8 +20,10 @@ utf8 ## Used by * [`AnnotationInfo`](../datatypes/annotation_info.md) +* [`GraphNode`](../components/graph_node.md?speculative-link) * [`MediaType`](../components/media_type.md) * [`Name`](../components/name.md) * [`TextLogLevel`](../components/text_log_level.md) * [`Text`](../components/text.md) +* [`Utf8Pair`](../datatypes/utf8pair.md?speculative-link) * [`VisibleTimeRange`](../datatypes/visible_time_range.md) diff --git a/docs/content/reference/types/datatypes/utf8pair.md b/docs/content/reference/types/datatypes/utf8pair.md new file mode 100644 index 000000000000..a1c4c272f99b --- /dev/null +++ b/docs/content/reference/types/datatypes/utf8pair.md @@ -0,0 +1,36 @@ +--- +title: "Utf8Pair" +--- + + +Stores a tuple of UTF-8 strings. + +## Fields +#### `first` +Type: [`Utf8`](../datatypes/utf8.md) + +The first string. + +#### `second` +Type: [`Utf8`](../datatypes/utf8.md) + +The second string. + + +## Arrow datatype +``` +Struct { + first: utf8 + second: utf8 +} +``` + +## API reference links + * 🌊 [C++ API docs for `Utf8Pair`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1datatypes_1_1Utf8Pair.html?speculative-link) + * 🐍 [Python API docs for `Utf8Pair`](https://ref.rerun.io/docs/python/stable/common/datatypes?speculative-link#rerun.datatypes.Utf8Pair) + * 🦀 [Rust API docs for `Utf8Pair`](https://docs.rs/rerun/latest/rerun/datatypes/struct.Utf8Pair.html?speculative-link) + + +## Used by + +* [`GraphEdge`](../components/graph_edge.md?speculative-link) diff --git a/docs/content/reference/types/views.md b/docs/content/reference/types/views.md index 3593e261a958..7b241e9aa2a2 100644 --- a/docs/content/reference/types/views.md +++ b/docs/content/reference/types/views.md @@ -9,6 +9,7 @@ Views are the panels shown in the viewer's viewport and the primary means of ins * [`BarChartView`](views/bar_chart_view.md): A bar chart view. * [`DataframeView`](views/dataframe_view.md): A view to display any data in a tabular form. +* [`GraphView`](views/graph_view.md): A graph view to display time-variying, directed or undirected graph visualization. * [`MapView`](views/map_view.md): A 2D map view to display geospatial primitives. * [`Spatial2DView`](views/spatial2d_view.md): For viewing spatial 2D data. * [`Spatial3DView`](views/spatial3d_view.md): For viewing spatial 3D data. diff --git a/docs/content/reference/types/views/.gitattributes b/docs/content/reference/types/views/.gitattributes index bf43e7d7a6a8..f996216907c6 100644 --- a/docs/content/reference/types/views/.gitattributes +++ b/docs/content/reference/types/views/.gitattributes @@ -3,6 +3,7 @@ .gitattributes linguist-generated=true bar_chart_view.md linguist-generated=true dataframe_view.md linguist-generated=true +graph_view.md linguist-generated=true map_view.md linguist-generated=true spatial2d_view.md linguist-generated=true spatial3d_view.md linguist-generated=true diff --git a/docs/content/reference/types/views/graph_view.md b/docs/content/reference/types/views/graph_view.md new file mode 100644 index 000000000000..f6830c618ed6 --- /dev/null +++ b/docs/content/reference/types/views/graph_view.md @@ -0,0 +1,37 @@ +--- +title: "GraphView" +--- + + +A graph view to display time-variying, directed or undirected graph visualization. + +## Properties + +### `visual_bounds` +Everything within these bounds is guaranteed to be visible. + +Somethings outside of these bounds may also be visible due to letterboxing. + +## API reference links + * 🐍 [Python API docs for `GraphView`](https://ref.rerun.io/docs/python/stable/common/blueprint_views?speculative-link#rerun.blueprint.views.GraphView) + +## Example + +### Use a blueprint to create a graph view. + +snippet: views/graph + + + + + + + + + + +## Visualized archetypes + +* [`GraphEdges`](../archetypes/graph_edges.md) +* [`GraphNodes`](../archetypes/graph_nodes.md) + diff --git a/docs/snippets/all/archetypes/graph_directed.cpp b/docs/snippets/all/archetypes/graph_directed.cpp new file mode 100644 index 000000000000..3f09463b0eac --- /dev/null +++ b/docs/snippets/all/archetypes/graph_directed.cpp @@ -0,0 +1,23 @@ +//! Log a simple directed graph. + +#include + +int main() { + const auto rec = rerun::RecordingStream("rerun_example_graph_directed"); + rec.spawn().exit_on_failure(); + + rec.log( + "simple", + rerun::GraphNodes({"a", "b", "c"}) + .with_positions({{0.0, 100.0}, {-100.0, 0.0}, {100.0, 0.0}}) + .with_labels({"A", "B", "C"}) + ); + + // Note: We log to the same entity here. + rec.log( + "simple", + rerun::GraphEdges({{"a", "b"}, {"b", "c"}, {"c", "a"}}) + // Graphs are undirected by default. + .with_graph_type(rerun::components::GraphType::Directed) + ); +} diff --git a/docs/snippets/all/archetypes/graph_directed.py b/docs/snippets/all/archetypes/graph_directed.py new file mode 100644 index 000000000000..63a803138a3b --- /dev/null +++ b/docs/snippets/all/archetypes/graph_directed.py @@ -0,0 +1,16 @@ +"""Log a simple directed graph.""" + +import rerun as rr + +rr.init("rerun_example_graph_directed", spawn=True) + +rr.log( + "simple", + rr.GraphNodes( + node_ids=["a", "b", "c"], positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"] + ), +) +rr.log( + "simple", + rr.GraphEdges(edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="directed"), +) diff --git a/docs/snippets/all/archetypes/graph_directed.rs b/docs/snippets/all/archetypes/graph_directed.rs new file mode 100644 index 000000000000..be05ad8bd407 --- /dev/null +++ b/docs/snippets/all/archetypes/graph_directed.rs @@ -0,0 +1,19 @@ +//! Log a simple directed graph. + +fn main() -> Result<(), Box> { + let rec = rerun::RecordingStreamBuilder::new("rerun_example_graph_directed").spawn()?; + + rec.log( + "simple", + &rerun::GraphNodes::new(["a", "b", "c"]) + .with_positions([(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)]) + .with_labels(["A", "B", "C"]), + )?; + // Note: We log to the same entity here. + rec.log( + "simple", + &rerun::GraphEdges::new([("a", "b"), ("b", "c"), ("c", "a")]).with_directed_edges(), + )?; + + Ok(()) +} diff --git a/docs/snippets/all/archetypes/graph_undirected.cpp b/docs/snippets/all/archetypes/graph_undirected.cpp new file mode 100644 index 000000000000..2d7e82ddba24 --- /dev/null +++ b/docs/snippets/all/archetypes/graph_undirected.cpp @@ -0,0 +1,23 @@ +//! Log a simple undirected graph. + +#include + +int main() { + const auto rec = rerun::RecordingStream("rerun_example_graph_undirected"); + rec.spawn().exit_on_failure(); + + rec.log( + "simple", + rerun::GraphNodes({"a", "b", "c"}) + .with_positions({{0.0, 100.0}, {-100.0, 0.0}, {100.0, 0.0}}) + .with_labels({"A", "B", "C"}) + ); + + // Note: We log to the same entity here. + rec.log( + "simple", + rerun::GraphEdges({{"a", "b"}, {"b", "c"}, {"c", "a"}}) + // Optional: graphs are undirected by default. + .with_graph_type(rerun::components::GraphType::Undirected) + ); +} diff --git a/docs/snippets/all/archetypes/graph_undirected.py b/docs/snippets/all/archetypes/graph_undirected.py new file mode 100644 index 000000000000..2397bfc31c5b --- /dev/null +++ b/docs/snippets/all/archetypes/graph_undirected.py @@ -0,0 +1,16 @@ +"""Log a simple undirected graph.""" + +import rerun as rr + +rr.init("rerun_example_graph_undirected", spawn=True) + +rr.log( + "simple", + rr.GraphNodes( + node_ids=["a", "b", "c"], positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"] + ), +) +rr.log( + "simple", + rr.GraphEdges(edges=[("a", "b"), ("b", "c"), ("c", "a")], graph_type="undirected"), +) diff --git a/docs/snippets/all/archetypes/graph_undirected.rs b/docs/snippets/all/archetypes/graph_undirected.rs new file mode 100644 index 000000000000..affd0ec3d0ee --- /dev/null +++ b/docs/snippets/all/archetypes/graph_undirected.rs @@ -0,0 +1,19 @@ +//! Log a simple undirected graph. + +fn main() -> Result<(), Box> { + let rec = rerun::RecordingStreamBuilder::new("rerun_example_graph_undirected").spawn()?; + + rec.log( + "simple", + &rerun::GraphNodes::new(["a", "b", "c"]) + .with_positions([(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)]) + .with_labels(["A", "B", "C"]), + )?; + // Note: We log to the same entity here. + rec.log( + "simple", + &rerun::GraphEdges::new([("a", "b"), ("b", "c"), ("c", "a")]).with_undirected_edges(), // Optional: graphs are undirected by default. + )?; + + Ok(()) +} diff --git a/docs/snippets/all/views/graph.py b/docs/snippets/all/views/graph.py new file mode 100644 index 000000000000..8ec843301ad4 --- /dev/null +++ b/docs/snippets/all/views/graph.py @@ -0,0 +1,26 @@ +"""Use a blueprint to customize a graph view.""" + +import rerun as rr +import rerun.blueprint as rrb + +rr.init("rerun_example_graph_view", spawn=True) + +rr.log( + "simple", + rr.GraphNodes( + node_ids=["a", "b", "c"], positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"] + ), +) + +# Create a Spatial2D view to display the points. +blueprint = rrb.Blueprint( + rrb.GraphView( + origin="/", + name="Graph", + # Note that this translates the viewbox. + visual_bounds=rrb.VisualBounds2D(x_range=[-150, 150], y_range=[-50, 150]), + ), + collapse_panels=True, +) + +rr.send_blueprint(blueprint) diff --git a/examples/manifest.toml b/examples/manifest.toml index 87f7fa7f3fe8..ce8d9b4d3376 100644 --- a/examples/manifest.toml +++ b/examples/manifest.toml @@ -148,6 +148,8 @@ examples = [ "plots", "live_scrolling_plot", "raw_mesh", + "graph_lattice", + "graph_binary_tree", ] # These are examples that we explicitly exclude from our website. You can check that all examples are either included diff --git a/examples/python/README.md b/examples/python/README.md index bd24e5da5d11..2f2389c6d2ca 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -75,4 +75,6 @@ Some examples will download a small datasets before they run. They will do so th ## Contributions welcome Feel free to open a PR to add a new example! +If you add an example, please make sure to add the corresponding entry to the top-level `pixi.toml` file as well, otherwise it will not be picked up. + See [`CONTRIBUTING.md`](../../CONTRIBUTING.md) for details on how to contribute. diff --git a/examples/python/graph_binary_tree/README.md b/examples/python/graph_binary_tree/README.md new file mode 100644 index 000000000000..1165823aa39f --- /dev/null +++ b/examples/python/graph_binary_tree/README.md @@ -0,0 +1,28 @@ + + +This example shows a binary tree that uses `GraphNodes` and `GraphEdges` together with fixed node positions. + + + + + + + + + +## Used Rerun types +[`GraphNodes`](https://www.rerun.io/docs/reference/types/archetypes/graph_nodes?speculative-link), +[`GraphEdges`](https://www.rerun.io/docs/reference/types/archetypes/graph_edges?speculative-link) + +## Run the code + +```bash +pip install -e examples/python/graph_binary_tree +python -m graph_binary_tree +``` diff --git a/examples/python/graph_binary_tree/graph_binary_tree.py b/examples/python/graph_binary_tree/graph_binary_tree.py new file mode 100644 index 000000000000..96f9a2c869a1 --- /dev/null +++ b/examples/python/graph_binary_tree/graph_binary_tree.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Examples of logging graph data to Rerun.""" + +from __future__ import annotations + +import argparse + +import rerun as rr + +DESCRIPTION = """ +# Binary tree +This is a minimal example that logs a time-varying binary tree to Rerun. + +The full source code for this example is available +[on GitHub](https://github.com/rerun-io/rerun/blob/latest/examples/python/graph_binary_tree?speculative-link). +""".strip() + +s = 3 # scaling factor for the positions + +# Potentially unbalanced and not sorted binary tree. :nerd_face:. +# :warning: The nodes have to be unique, which is why we use `5_0`… + +NodeId = str + + +class NodeInfo: + def __init__(self, label: str, pos: tuple[float, float]) -> None: + self.label = label + self.pos = pos + + +all_nodes: dict[NodeId, NodeInfo] = { + "1": NodeInfo(label="1", pos=(0 * s, 0 * s)), + "7": NodeInfo(label="7", pos=(-20 * s, 30 * s)), + "2": NodeInfo(label="2", pos=(-30 * s, 60 * s)), + "6": NodeInfo(label="6", pos=(-10 * s, 60 * s)), + "5_0": NodeInfo(label="5", pos=(-20 * s, 90 * s)), + "11": NodeInfo(label="11", pos=(0 * s, 90 * s)), + "9_0": NodeInfo(label="9", pos=(20 * s, 30 * s)), + "9_1": NodeInfo(label="9", pos=(30 * s, 60 * s)), + "5_1": NodeInfo(label="5", pos=(20 * s, 90 * s)), +} + + +class Level: + def __init__(self, nodes: list[NodeId], edges: list[tuple[NodeId, NodeId]]): + self.nodes = nodes + self.edges = edges + + +levels: list[Level] = [ + Level(nodes=["1"], edges=[]), + Level(nodes=["1", "7", "9_0"], edges=[("1", "7"), ("1", "9_0")]), + Level( + nodes=["1", "7", "9_0", "2", "6", "9_1"], + edges=[("1", "7"), ("1", "9_0"), ("7", "2"), ("7", "6"), ("9_0", "9_1")], + ), + Level( + nodes=["1", "7", "9_0", "2", "6", "9_1", "5_0", "11", "5_1"], + edges=[ + ("1", "7"), + ("1", "9_0"), + ("7", "2"), + ("7", "6"), + ("9_0", "9_1"), + ("6", "5_0"), + ("6", "11"), + ("9_1", "5_1"), + ], + ), +] + + +def log_data() -> None: + rr.log("description", rr.TextDocument(DESCRIPTION, media_type=rr.MediaType.MARKDOWN), static=True) + + t = 0 + for level in levels: + if len(level.nodes) > 0: + t = t + 1 + rr.set_time_seconds("stable_time", t) + rr.log( + "binary_tree", + rr.GraphNodes( + level.nodes, + labels=list(map(lambda n: all_nodes[n].label, level.nodes)), + positions=list(map(lambda n: all_nodes[n].pos, level.nodes)), + ), + ) + + if len(level.edges) > 0: + t = t + 1 + rr.set_time_seconds("stable_time", t) + rr.log("binary_tree", rr.GraphEdges(level.edges)) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Logs a binary tree with associated positions using the Rerun SDK.") + rr.script_add_args(parser) + args = parser.parse_args() + + rr.script_setup(args, "rerun_example_graph_binary_tree") + log_data() + rr.script_teardown(args) + + +if __name__ == "__main__": + main() diff --git a/examples/python/graph_binary_tree/pyproject.toml b/examples/python/graph_binary_tree/pyproject.toml new file mode 100644 index 000000000000..a10385ee722a --- /dev/null +++ b/examples/python/graph_binary_tree/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "graph_binary_tree" +version = "0.1.0" +readme = "README.md" +dependencies = ["rerun-sdk"] + +[project.scripts] +graph_binary_tree = "graph_binary_tree:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python/graph_lattice/README.md b/examples/python/graph_lattice/README.md new file mode 100644 index 000000000000..ed764409c37c --- /dev/null +++ b/examples/python/graph_lattice/README.md @@ -0,0 +1,29 @@ + + +This example shows different attributes that you can associate with nodes in a graph. +Since no explicit positions are passed for the nodes, Rerun will layout the graph automatically. + + + + + + + + + +## Used Rerun types +[`GraphNodes`](https://www.rerun.io/docs/reference/types/archetypes/graph_nodes?speculative-link), +[`GraphEdges`](https://www.rerun.io/docs/reference/types/archetypes/graph_edges?speculative-link) + +## Run the code + +```bash +pip install -e examples/python/graph_lattice +python -m graph_lattice +``` diff --git a/examples/python/graph_lattice/graph_lattice.py b/examples/python/graph_lattice/graph_lattice.py new file mode 100644 index 000000000000..21eb327df1ca --- /dev/null +++ b/examples/python/graph_lattice/graph_lattice.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Examples of logging graph data to Rerun and performing a force-based layout.""" + +from __future__ import annotations + +import argparse +import itertools + +import rerun as rr + +NUM_NODES = 10 + +DESCRIPTION = """ +# Graph Lattice +This is a minimal example that logs a graph (node-link diagram) that represents a lattice. + +In this example, the node positions—and therefore the graph layout—are computed by Rerun internally. + +The full source code for this example is available +[on GitHub](https://github.com/rerun-io/rerun/blob/latest/examples/python/graph_lattice?speculative-link). +""".strip() + + +def log_data() -> None: + rr.log("description", rr.TextDocument(DESCRIPTION, media_type=rr.MediaType.MARKDOWN), static=True) + + coordinates = itertools.product(range(NUM_NODES), range(NUM_NODES)) + + nodes, colors = zip(*[ + ( + str(i), + rr.components.Color([round((x / (NUM_NODES - 1)) * 255), round((y / (NUM_NODES - 1)) * 255), 0]), + ) + for i, (x, y) in enumerate(coordinates) + ]) + + rr.log( + "/lattice", + rr.GraphNodes( + nodes, + colors=colors, + labels=[f"({x}, {y})" for x, y in itertools.product(range(NUM_NODES), range(NUM_NODES))], + ), + static=True, + ) + + edges = [] + for x, y in itertools.product(range(NUM_NODES), range(NUM_NODES)): + if y > 0: + source = (y - 1) * NUM_NODES + x + target = y * NUM_NODES + x + edges.append((str(source), str(target))) + if x > 0: + source = y * NUM_NODES + (x - 1) + target = y * NUM_NODES + x + edges.append((str(source), str(target))) + + rr.log("/lattice", rr.GraphEdges(edges, graph_type="directed"), static=True) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Logs a graph lattice using the Rerun SDK.") + rr.script_add_args(parser) + args = parser.parse_args() + + rr.script_setup(args, "rerun_example_graph_lattice") + log_data() + rr.script_teardown(args) + + +if __name__ == "__main__": + main() diff --git a/examples/python/graph_lattice/pyproject.toml b/examples/python/graph_lattice/pyproject.toml new file mode 100644 index 000000000000..128b7879d28e --- /dev/null +++ b/examples/python/graph_lattice/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "graph_lattice" +version = "0.1.0" +readme = "README.md" +dependencies = ["rerun-sdk"] + +[project.scripts] +graph_lattice = "graph_lattice:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/rust/graph_binary_tree/Cargo.toml b/examples/rust/graph_binary_tree/Cargo.toml new file mode 100644 index 000000000000..456b7cbe3989 --- /dev/null +++ b/examples/rust/graph_binary_tree/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "graph_binary_tree" +version = "0.21.0-alpha.1+dev" +edition = "2021" +rust-version = "1.79" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +rerun = { path = "../../../crates/top/rerun", features = ["clap"] } + +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } diff --git a/examples/rust/graph_binary_tree/README.md b/examples/rust/graph_binary_tree/README.md new file mode 100644 index 000000000000..110ff4de97b1 --- /dev/null +++ b/examples/rust/graph_binary_tree/README.md @@ -0,0 +1,3 @@ +# graph_binary_tree + +Demonstrates visualization of a graph that varies over time. diff --git a/examples/rust/graph_binary_tree/src/main.rs b/examples/rust/graph_binary_tree/src/main.rs new file mode 100644 index 000000000000..1f820fa28f0a --- /dev/null +++ b/examples/rust/graph_binary_tree/src/main.rs @@ -0,0 +1,155 @@ +//! Shows how to draw a graph that varies over time. +//! Please not that this example makes use of fixed positions. +//! +//! Usage: +//! ``` +//! cargo run -p graph_binary_tree -- --connect +//! ``` + +use rerun::{external::re_log, GraphEdges, GraphNodes}; +use std::collections::HashMap; + +#[derive(Debug, clap::Parser)] +#[clap(author, version, about)] +pub struct Args { + #[command(flatten)] + rerun: rerun::clap::RerunArgs, +} + +fn main() -> anyhow::Result<()> { + re_log::setup_logging(); + + use clap::Parser as _; + let args = Args::parse(); + + let (rec, _serve_guard) = args.rerun.init("rerun_example_graph_binary_tree")?; + + let s = 3.0; // scaling factor for the positions + + // Potentially unbalanced and not sorted binary tree. :nerd_face:. + // :warning: The nodes have to be unique, which is why we use `5_0`… + + // (label, position) + type NodeInfo = (&'static str, (f32, f32)); + + struct Level<'a> { + nodes: &'a [&'a str], + edges: &'a [(&'a str, &'a str)], + } + + let nodes_unsorted: HashMap<&str, NodeInfo> = [ + ("1", ("1", (0.0 * s, 0.0 * s))), + ("7", ("7", (-20.0 * s, 30.0 * s))), + ("2", ("2", (-30.0 * s, 60.0 * s))), + ("6", ("6", (-10.0 * s, 60.0 * s))), + ("5_0", ("5", (-20.0 * s, 90.0 * s))), + ("11", ("11", (0.0 * s, 90.0 * s))), + ("9_0", ("9", (20.0 * s, 30.0 * s))), + ("9_1", ("9", (30.0 * s, 60.0 * s))), + ("5_1", ("5", (20.0 * s, 90.0 * s))), + ] + .into_iter() + .collect(); + + let levels_unsorted: Vec = vec![ + Level { + nodes: &["1"], + edges: &[], + }, + Level { + nodes: &["1", "7", "9_0"], + edges: &[("1", "7"), ("1", "9_0")], + }, + Level { + nodes: &["1", "7", "9_0", "2", "6", "9_1"], + edges: &[ + ("1", "7"), + ("1", "9_0"), + ("7", "2"), + ("7", "6"), + ("9_0", "9_1"), + ], + }, + Level { + nodes: &["1", "7", "9_0", "2", "6", "9_1", "5_0", "11", "5_1"], + edges: &[ + ("1", "7"), + ("1", "9_0"), + ("7", "2"), + ("7", "6"), + ("9_0", "9_1"), + ("6", "5_0"), + ("6", "11"), + ("9_1", "5_1"), + ], + }, + ]; + + let nodes_sorted: HashMap<&str, NodeInfo> = [ + ("6", ("6", (0.0 * s, 0.0 * s))), + ("5_0", ("5", (-20.0 * s, 30.0 * s))), + ("9_0", ("9", (20.0 * s, 30.0 * s))), + ] + .into_iter() + .collect(); + + let levels_sorted: Vec = vec![ + Level { + nodes: &["6"], + edges: &[], + }, + Level { + nodes: &["6", "5_0", "9_0"], + edges: &[("6", "5_0"), ("6", "9_0"), ("1", "6"), ("1", "42")], + }, + ]; + + let mut t = 0; + for level in levels_unsorted { + if !level.nodes.is_empty() { + t += 1; + rec.set_time_seconds("stable_time", t as f64); + let _ = rec.log( + "unsorted", + &GraphNodes::new(level.nodes.iter().copied()) + .with_labels(level.nodes.iter().map(|n| nodes_unsorted[n].0)) + .with_positions(level.nodes.iter().map(|n| nodes_unsorted[n].1)), + ); + } + + if !level.edges.is_empty() { + t += 1; + rec.set_time_seconds("stable_time", t as f64); + let _ = rec.log("unsorted", &GraphEdges::new(level.edges)); + } + } + + let entity_offset_x = 200.0; + + for level in levels_sorted { + if !level.nodes.is_empty() { + t += 1; + rec.set_time_seconds("stable_time", t as f64); + let _ = rec.log( + "sorted", + &GraphNodes::new(level.nodes.iter().copied()) + .with_labels(level.nodes.iter().map(|n| nodes_sorted[n].0)) + .with_positions(level.nodes.iter().map(|n| { + let (x, y) = nodes_sorted[n].1; + [x + entity_offset_x, y] + })), + ); + } + + if !level.edges.is_empty() { + t += 1; + rec.set_time_seconds("stable_time", t as f64); + let _ = rec.log( + "sorted", + &GraphEdges::new(level.edges).with_directed_edges(), + ); + } + } + + Ok(()) +} diff --git a/examples/rust/graph_lattice/Cargo.toml b/examples/rust/graph_lattice/Cargo.toml new file mode 100644 index 000000000000..837b88dc5e84 --- /dev/null +++ b/examples/rust/graph_lattice/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "graph_lattice" +version = "0.21.0-alpha.1+dev" +edition = "2021" +rust-version = "1.79" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +rerun = { path = "../../../crates/top/rerun", features = ["clap"] } + +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } +itertools = "0.13" diff --git a/examples/rust/graph_lattice/README.md b/examples/rust/graph_lattice/README.md new file mode 100644 index 000000000000..c714a94be6e1 --- /dev/null +++ b/examples/rust/graph_lattice/README.md @@ -0,0 +1,3 @@ +# graph_lattice + +Demonstrates graph layout of a lattice without explicit positions. diff --git a/examples/rust/graph_lattice/src/main.rs b/examples/rust/graph_lattice/src/main.rs new file mode 100644 index 000000000000..8a14d23356e6 --- /dev/null +++ b/examples/rust/graph_lattice/src/main.rs @@ -0,0 +1,64 @@ +//! Shows how to draw a graph with various node properties. +//! +//! Usage: +//! ``` +//! cargo run -p graph_lattice -- --connect +//! ``` + +use itertools::Itertools as _; +use rerun::{external::re_log, Color, GraphEdges, GraphNodes}; + +#[derive(Debug, clap::Parser)] +#[clap(author, version, about)] +pub struct Args { + #[command(flatten)] + rerun: rerun::clap::RerunArgs, +} + +const NUM_NODES: usize = 10; + +fn main() -> anyhow::Result<()> { + re_log::setup_logging(); + + use clap::Parser as _; + let args = Args::parse(); + + let (rec, _serve_guard) = args.rerun.init("rerun_example_graph_lattice")?; + + let coordinates = (0..NUM_NODES).cartesian_product(0..NUM_NODES); + + let (nodes, colors): (Vec<_>, Vec<_>) = coordinates + .clone() + .enumerate() + .map(|(i, (x, y))| { + let r = ((x as f32 / (NUM_NODES - 1) as f32) * 255.0).round() as u8; + let g = ((y as f32 / (NUM_NODES - 1) as f32) * 255.0).round() as u8; + (i.to_string(), Color::from_rgb(r, g, 0)) + }) + .unzip(); + + rec.log_static( + "/lattice", + &GraphNodes::new(nodes) + .with_colors(colors) + .with_labels(coordinates.clone().map(|(x, y)| format!("({}, {})", x, y))), + )?; + + let mut edges = Vec::new(); + for (x, y) in coordinates { + if y > 0 { + let source = (y - 1) * NUM_NODES + x; + let target = y * NUM_NODES + x; + edges.push((source.to_string(), target.to_string())); + } + if x > 0 { + let source = y * NUM_NODES + (x - 1); + let target = y * NUM_NODES + x; + edges.push((source.to_string(), target.to_string())); + } + } + + rec.log_static("/lattice", &GraphEdges::new(edges).with_directed_edges())?; + + Ok(()) +} diff --git a/pixi.lock b/pixi.lock index b98b86c602f1..fff4677f244d 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2542,6 +2542,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -2937,6 +2939,8 @@ environments: - pypi: examples/python/dicom_mri - pypi: examples/python/dna - pypi: examples/python/drone_lidar + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/incremental_logging - pypi: examples/python/lidar - pypi: examples/python/live_camera_edge_detection @@ -3313,6 +3317,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -3691,6 +3697,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -4068,6 +4076,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -4489,6 +4499,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -4836,6 +4848,8 @@ environments: - pypi: examples/python/dicom_mri - pypi: examples/python/dna - pypi: examples/python/drone_lidar + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/incremental_logging - pypi: examples/python/lidar - pypi: examples/python/live_camera_edge_detection @@ -5165,6 +5179,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -5496,6 +5512,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -5827,6 +5845,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -9061,6 +9081,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -9550,6 +9572,8 @@ environments: - pypi: examples/python/dicom_mri - pypi: examples/python/dna - pypi: examples/python/drone_lidar + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/incremental_logging - pypi: examples/python/lidar - pypi: examples/python/live_camera_edge_detection @@ -10019,6 +10043,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -10490,6 +10516,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -10940,6 +10968,8 @@ environments: - pypi: examples/python/drone_lidar - pypi: examples/python/face_tracking - pypi: examples/python/gesture_detection + - pypi: examples/python/graph_binary_tree + - pypi: examples/python/graph_lattice - pypi: examples/python/human_pose_tracking - pypi: examples/python/incremental_logging - pypi: examples/python/lidar @@ -13081,21 +13111,6 @@ packages: - tqdm - trimesh editable: true -- kind: pypi - name: arkit-scenes - version: 0.1.0 - path: examples/python/arkit_scenes - sha256: 1ea1defa403966ebbc7e54ff7b92f65fdb6c98ee3e242d8af0be46023ed87961 - requires_dist: - - matplotlib - - numpy - - opencv-python - - pandas - - rerun-sdk - - scipy - - tqdm - - trimesh - editable: true - kind: pypi name: arrow version: 1.3.0 @@ -14937,20 +14952,6 @@ packages: - jinja2 ; extra == 'compiler' - protobuf ; extra == 'compiler' requires_python: '>=3.6' -- kind: pypi - name: betterproto - version: 1.2.5 - url: https://files.pythonhosted.org/packages/ff/2e/abfed7a721928e14aeb900182ff695be474c4ee5f07ef0874cc5ecd5b0b1/betterproto-1.2.5.tar.gz - sha256: 74a3ab34646054f674d236d1229ba8182dc2eae86feb249b8590ef496ce9803d - requires_dist: - - grpclib - - stringcase - - dataclasses ; python_full_version < '3.7' - - backports-datetime-fromisoformat ; python_full_version < '3.7' - - black ; extra == 'compiler' - - jinja2 ; extra == 'compiler' - - protobuf ; extra == 'compiler' - requires_python: '>=3.6' - kind: conda name: binaryen version: '117' @@ -15242,26 +15243,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: blueprint - version: 0.1.0 - path: examples/python/blueprint - sha256: d9a358e5994ec1e9144942903e46148b16825344cddc19e7188b285f59bc61c1 - requires_dist: - - numpy - - rerun-sdk - editable: true -- kind: pypi - name: blueprint-stocks - version: 0.1.0 - path: examples/python/blueprint_stocks - sha256: 7c8b6805f08610837014175d9d0212815a91c3197756cdbbce836a2f15e40eea - requires_dist: - - humanize - - rerun-sdk - - yfinance - requires_python: '>=3.8' - editable: true - kind: pypi name: blueprint-stocks version: 0.1.0 @@ -16571,15 +16552,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: clock - version: 0.1.0 - path: examples/python/clock - sha256: 1ae48a7222b2fc2bd9942a31bb48fefb34225a946859ad95c25ad00bfb754cd7 - requires_dist: - - numpy - - rerun-sdk - editable: true - kind: conda name: cmake version: 3.27.6 @@ -16941,29 +16913,13 @@ packages: sha256: 8ae055c0b8b0dd7757e4e666f6163172859044d4090830aecbec3460cdb318ee requires_dist: - accelerate - - opencv-python - - pillow - diffusers==0.27.2 - numpy - - torch==2.2.2 - - transformers - - rerun-sdk - requires_python: '>=3.10' - editable: true -- kind: pypi - name: controlnet - version: 0.1.0 - path: examples/python/controlnet - sha256: 8ae055c0b8b0dd7757e4e666f6163172859044d4090830aecbec3460cdb318ee - requires_dist: - - accelerate - opencv-python - pillow - - diffusers==0.27.2 - - numpy + - rerun-sdk - torch==2.2.2 - transformers - - rerun-sdk requires_python: '>=3.10' editable: true - kind: pypi @@ -17219,14 +17175,6 @@ packages: requires_dist: - rerun-sdk editable: true -- kind: pypi - name: dataframe-query - version: 0.1.0 - path: examples/python/dataframe_query - sha256: cfd25d2d2872eef690801817f4a3bf4660d4b232cc594d0bcc3ee70a783dfd46 - requires_dist: - - rerun-sdk - editable: true - kind: conda name: dav1d version: 1.2.1 @@ -17377,22 +17325,7 @@ packages: - numpy - opencv-contrib-python>4.6 - pillow - - requests>=2.31,<3 - - rerun-sdk - - timm==0.9.11 - - torch==2.2.2 - - transformers - editable: true -- kind: pypi - name: detect-and-track-objects - version: 0.1.0 - path: examples/python/detect_and_track_objects - sha256: 2c2d3d8b61d6a0bf44051fd7052e3087fd4762b9c434586078ea3d179940cba1 - requires_dist: - - numpy - - opencv-contrib-python>4.6 - - pillow - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - timm==0.9.11 - torch==2.2.2 @@ -17407,22 +17340,9 @@ packages: - dicom-numpy==0.6.2 - numpy - pydicom==2.3.0 - - requests>=2.31,<3 - - rerun-sdk - - types-requests>=2.31,<3 - editable: true -- kind: pypi - name: dicom-mri - version: 0.1.0 - path: examples/python/dicom_mri - sha256: 98cb91dc5758ae59e3cd0fb797f86f40fcf627f63e659365806f59feed4618d8 - requires_dist: - - dicom-numpy==0.6.2 - - numpy - - pydicom==2.3.0 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - - types-requests>=2.31,<3 + - types-requests<3,>=2.31 editable: true - kind: pypi name: dicom-numpy @@ -17530,16 +17450,6 @@ packages: - rerun-sdk - scipy editable: true -- kind: pypi - name: dna - version: 0.1.0 - path: examples/python/dna - sha256: 15dd8b0ce0ee55262916ea9bc8fb93c72c2012cb01a78e6d24a526d92537eab4 - requires_dist: - - numpy - - rerun-sdk - - scipy - editable: true - kind: conda name: double-conversion version: 3.3.0 @@ -17674,18 +17584,6 @@ packages: - rerun-sdk - tqdm editable: true -- kind: pypi - name: drone-lidar - version: 0.1.0 - path: examples/python/drone_lidar - sha256: 4de8d4135d07f9b0389eeb8a3616a5a9941a868b4d876cc91d8b5351c3b8984d - requires_dist: - - laspy - - numpy - - requests - - rerun-sdk - - tqdm - editable: true - kind: conda name: exceptiongroup version: 1.2.2 @@ -17814,21 +17712,6 @@ packages: - tqdm requires_python: <3.12 editable: true -- kind: pypi - name: face-tracking - version: 0.1.0 - path: examples/python/face_tracking - sha256: b8725fe4d36c11aad2c6c936ba2b57c7f65a856aa179badca5d041db63119d55 - requires_dist: - - mediapipe==0.10.11 ; sys_platform != 'darwin' - - mediapipe==0.10.9 ; sys_platform == 'darwin' - - numpy - - opencv-python>4.6 - - requests - - rerun-sdk - - tqdm - requires_python: <3.12 - editable: true - kind: pypi name: fastjsonschema version: 2.20.0 @@ -18197,13 +18080,6 @@ packages: sha256: 961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf requires_dist: - termcolor -- kind: pypi - name: fire - version: 0.7.0 - url: https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz - sha256: 961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf - requires_dist: - - termcolor - kind: pypi name: flatbuffers version: 24.3.25 @@ -19228,22 +19104,7 @@ packages: - mediapipe==0.10.9 ; sys_platform == 'darwin' - numpy - opencv-python>4.9 - - requests>=2.31,<3 - - rerun-sdk - - tqdm - requires_python: <3.12 - editable: true -- kind: pypi - name: gesture-detection - version: 0.1.0 - path: examples/python/gesture_detection - sha256: 36dfc4cc822ee47f7aa29ba951bab8a94e96b9fd737daa324a441e6962a620bd - requires_dist: - - mediapipe==0.10.11 ; sys_platform != 'darwin' - - mediapipe==0.10.9 ; sys_platform == 'darwin' - - numpy - - opencv-python>4.9 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - tqdm requires_python: <3.12 @@ -19621,15 +19482,6 @@ packages: - importlib-resources>=1.3 ; python_full_version < '3.9' and os_name == 'nt' - pytest ; extra == 'testing' requires_python: '>=3.9' -- kind: pypi - name: google-crc32c - version: 1.6.0 - url: https://files.pythonhosted.org/packages/67/72/c3298da1a3773102359c5a78f20dae8925f5ea876e37354415f68594a6fb/google_crc32c-1.6.0.tar.gz - sha256: 6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc - requires_dist: - - importlib-resources>=1.3 ; python_full_version < '3.9' and os_name == 'nt' - - pytest ; extra == 'testing' - requires_python: '>=3.9' - kind: pypi name: google-crc32c version: 1.6.0 @@ -19659,6 +19511,22 @@ packages: - protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2 - grpcio<2.0.0.dev0,>=1.44.0 ; extra == 'grpc' requires_python: '>=3.7' +- kind: pypi + name: graph-binary-tree + version: 0.1.0 + path: examples/python/graph_binary_tree + sha256: 8ceb8c2d1102f2f2476eb5fb9c79e717e520f92220709449f48138a3c84c6609 + requires_dist: + - rerun-sdk + editable: true +- kind: pypi + name: graph-lattice + version: 0.1.0 + path: examples/python/graph_lattice + sha256: f92a889e55062d414fbf9847d0b2b216b8e4bcaf8ee2965476de877102ee52f8 + requires_dist: + - rerun-sdk + editable: true - kind: conda name: graphite2 version: 1.3.13 @@ -19762,16 +19630,6 @@ packages: - multidict - protobuf>=3.20.0 ; extra == 'protobuf' requires_python: '>=3.7' -- kind: pypi - name: grpclib - version: 0.4.7 - url: https://files.pythonhosted.org/packages/79/b9/55936e462a5925190d7427e880b3033601d1effd13809b483d13a926061a/grpclib-0.4.7.tar.gz - sha256: 2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3 - requires_dist: - - h2<5,>=3.1.0 - - multidict - - protobuf>=3.20.0 ; extra == 'protobuf' - requires_python: '>=3.7' - kind: conda name: gxx version: 12.4.0 @@ -20337,21 +20195,7 @@ packages: - mediapipe==0.10.9 ; sys_platform == 'darwin' - numpy - opencv-python>4.6 - - requests>=2.31,<3 - - rerun-sdk - requires_python: <3.12 - editable: true -- kind: pypi - name: human-pose-tracking - version: 0.1.0 - path: examples/python/human_pose_tracking - sha256: 8a80b67528d3f6d0c82671dc5c36cf551faa4b879f4434f0d386d8ef85666e86 - requires_dist: - - mediapipe==0.10.11 ; sys_platform != 'darwin' - - mediapipe==0.10.9 ; sys_platform == 'darwin' - - numpy - - opencv-python>4.6 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk requires_python: <3.12 editable: true @@ -20677,15 +20521,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: incremental-logging - version: 0.1.0 - path: examples/python/incremental_logging - sha256: c1efe33868c31fe5a07ab5f6e60d28f856735a9e0b221ff96abd2e711d60e894 - requires_dist: - - numpy - - rerun-sdk - editable: true - kind: conda name: iniconfig version: 2.0.0 @@ -29036,27 +28871,6 @@ packages: - requests - rerun-sdk editable: true -- kind: pypi - name: lidar - version: 0.1.0 - path: examples/python/lidar - sha256: 10fe6d7b3a80959f913aada12c01bfecd6cd9854beaf6a8843a7ecd2215cd4bd - requires_dist: - - matplotlib - - numpy - - nuscenes-devkit - - requests - - rerun-sdk - editable: true -- kind: pypi - name: live-camera-edge-detection - version: 0.1.0 - path: examples/python/live_camera_edge_detection - sha256: f1edef43efce87f55726e3b5d6a2f813667968f8e8185873a74b9dc61c0f040f - requires_dist: - - opencv-python - - rerun-sdk - editable: true - kind: pypi name: live-camera-edge-detection version: 0.1.0 @@ -29075,27 +28889,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: live-scrolling-plot - version: 0.1.0 - path: examples/python/live_scrolling_plot - sha256: 1debab1814169399bb2ed23af2cd97a4693e7a4d4ee55e74bcb8804bf421e8fc - requires_dist: - - numpy - - rerun-sdk - editable: true -- kind: pypi - name: llm-embedding-ner - version: 0.1.0 - path: examples/python/llm_embedding_ner - sha256: 6f5925cbe333d529421ef9a5114f85317bdd8b4200c1e9ff6798dff5e3a7f16f - requires_dist: - - rerun-sdk - - torch - - transformers - - umap-learn - requires_python: <3.12 - editable: true - kind: pypi name: llm-embedding-ner version: 0.1.0 @@ -29228,14 +29021,6 @@ packages: requires_dist: - rerun-sdk editable: true -- kind: pypi - name: log-file - version: 0.1.0 - path: examples/python/log_file - sha256: fb6af8faeaac3e8d16da4ab40e26a73dd0e63483f34aa36298c32f7e39324fd3 - requires_dist: - - rerun-sdk - editable: true - kind: pypi name: lxml version: 5.3.0 @@ -29994,16 +29779,6 @@ packages: - scikit-image - scikit-learn requires_python: '>=3.5' -- kind: pypi - name: mesh-to-sdf - version: 0.0.15 - url: git+https://github.com/marian42/mesh_to_sdf.git@c9f26e6399f7fd8deb40c7fba02c7e74aca6c657 - requires_dist: - - pyopengl - - pyrender - - scikit-image - - scikit-learn - requires_python: '>=3.5' - kind: pypi name: mike version: 1.1.2 @@ -30029,24 +29804,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: minimal - version: 0.1.0 - path: examples/python/minimal - sha256: 871c1ec39ceb3af42679653369402d66672d4bb9850a727b27db05c16653c8dd - requires_dist: - - numpy - - rerun-sdk - editable: true -- kind: pypi - name: minimal-options - version: 0.1.0 - path: examples/python/minimal_options - sha256: 84d5a8787772da382454f2f3b44d54027a606bff043872dab559cc4604ac82f0 - requires_dist: - - numpy - - rerun-sdk - editable: true - kind: pypi name: minimal-options version: 0.1.0 @@ -30445,14 +30202,6 @@ packages: requires_dist: - rerun-sdk editable: true -- kind: pypi - name: multiprocess-logging - version: 0.1.0 - path: examples/python/multiprocess_logging - sha256: 90ae836d45110662ac53e73a092a5298ab67d89873eed81d1773dba601a62eb2 - requires_dist: - - rerun-sdk - editable: true - kind: pypi name: multitasking version: 0.0.11 @@ -30467,15 +30216,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: multithreading - version: 0.1.0 - path: examples/python/multithreading - sha256: 85b43cb06183386edd0a8820c0c9eb50398c197fd0da8ba5050f2cf2b24bc23e - requires_dist: - - numpy - - rerun-sdk - editable: true - kind: conda name: mypy version: 1.8.0 @@ -31430,18 +31170,6 @@ packages: - requests - rerun-sdk editable: true -- kind: pypi - name: nuscenes-dataset - version: 0.1.0 - path: examples/python/nuscenes_dataset - sha256: 78903b7670fac2b4c27637efc9aa6f63b7b70502ce3d5f88e4353aef5607cf40 - requires_dist: - - matplotlib - - numpy - - nuscenes-devkit - - requests - - rerun-sdk - editable: true - kind: pypi name: nuscenes-devkit version: 1.1.9 @@ -31469,19 +31197,9 @@ packages: path: examples/python/nv12 sha256: c8ca97c5d8c04037cd5eb9a65be7b1e7d667c11d4dba3ee9aad5956ccf926dc4 requires_dist: - - rerun-sdk>=0.10 - - opencv-python - numpy - editable: true -- kind: pypi - name: nv12 - version: 0.1.0 - path: examples/python/nv12 - sha256: c8ca97c5d8c04037cd5eb9a65be7b1e7d667c11d4dba3ee9aad5956ccf926dc4 - requires_dist: - - rerun-sdk>=0.10 - opencv-python - - numpy + - rerun-sdk>=0.10 editable: true - kind: pypi name: nvidia-cublas-cu12 @@ -31572,20 +31290,7 @@ packages: - betterproto[compiler] - numpy - opencv-python>4.6 - - requests>=2.31,<3 - - rerun-sdk - - scipy - editable: true -- kind: pypi - name: objectron - version: 0.1.0 - path: examples/python/objectron - sha256: b2be2b675353b4238e7778b1cef8351950832c32b5e5c34415601c030a421a27 - requires_dist: - - betterproto[compiler] - - numpy - - opencv-python>4.6 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - scipy editable: true @@ -31619,20 +31324,6 @@ packages: - tqdm requires_python: '>=3.10' editable: true -- kind: pypi - name: open-photogrammetry-format - version: 0.1.0 - path: examples/python/open_photogrammetry_format - sha256: 1bf1ac24e064bb75c7f5672b761e519b8b941354a6d9c44d824643ff64f15e80 - requires_dist: - - numpy - - pillow - - pyopf - - requests - - rerun-sdk - - tqdm - requires_python: '>=3.10' - editable: true - kind: conda name: opencv version: 4.10.0 @@ -32105,15 +31796,6 @@ packages: - requests - rerun-sdk editable: true -- kind: pypi - name: openstreetmap-data - version: 0.1.0 - path: examples/python/openstreetmap_data - sha256: 3b19fd64cb7d102a75ac934cd188cdd110fddca78c72701f10596f0ed8a0dff5 - requires_dist: - - requests - - rerun-sdk - editable: true - kind: pypi name: opt-einsum version: 3.4.0 @@ -33262,15 +32944,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: plots - version: 0.1.0 - path: examples/python/plots - sha256: 398c85932db816f766e2d703568060efe4520a6e734f91d69387bbdd143b3f97 - requires_dist: - - numpy - - rerun-sdk - editable: true - kind: conda name: pluggy version: 1.5.0 @@ -34436,16 +34109,6 @@ packages: - deprecated - dataclasses ; python_full_version == '3.6.*' requires_python: '>=3.6' -- kind: pypi - name: pygltflib - version: 1.16.2 - url: https://files.pythonhosted.org/packages/38/d7/0b8e35cb3ff69dd981e358e72e0a5632f847d4bd61876be04518cb4e075a/pygltflib-1.16.2.tar.gz - sha256: 4f9481f5841b0b8fb7b271b0414b394b503405260a6ee0cf2c330a5420d19b64 - requires_dist: - - dataclasses-json>=0.0.25 - - deprecated - - dataclasses ; python_full_version == '3.6.*' - requires_python: '>=3.6' - kind: pypi name: pygments version: 2.18.0 @@ -35107,18 +34770,7 @@ packages: sha256: 9006b1b7ca8bd9c90ba0bf0d7a00641b7dd13a6de76a2828f79ec5b853a4ef98 requires_dist: - numpy - - requests>=2.31,<3 - - rerun-sdk - - trimesh==3.15.2 - editable: true -- kind: pypi - name: raw-mesh - version: 0.1.0 - path: examples/python/raw_mesh - sha256: 9006b1b7ca8bd9c90ba0bf0d7a00641b7dd13a6de76a2828f79ec5b853a4ef98 - requires_dist: - - numpy - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - trimesh==3.15.2 editable: true @@ -35343,6 +34995,18 @@ packages: - jupyterlab ; extra == 'dev' - hatch ; extra == 'dev' editable: true +- kind: pypi + name: rerun-notebook + version: 0.21.0a1+dev + path: rerun_notebook + sha256: 047bf251505a9b46f02e7f700fe4073247fa2c95f8dd36f6f2f628e0ade8c055 + requires_dist: + - anywidget + - jupyter-ui-poll + - watchfiles ; extra == 'dev' + - jupyterlab ; extra == 'dev' + - hatch ; extra == 'dev' + editable: true - kind: pypi name: rerun-sdk version: 0.17.0 @@ -35450,19 +35114,7 @@ packages: requires_dist: - numpy - opencv-python>4.6 - - requests>=2.31,<3 - - rerun-sdk - - tqdm - editable: true -- kind: pypi - name: rgbd - version: 0.1.0 - path: examples/python/rgbd - sha256: b2ef153b0bedd672c3e0ce89b7be1f64f4344b2b75d71748899faea270383fa2 - requires_dist: - - numpy - - opencv-python>4.6 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - tqdm editable: true @@ -35577,15 +35229,6 @@ packages: - numpy - rerun-sdk editable: true -- kind: pypi - name: rrt-star - version: 0.1.0 - path: examples/python/rrt_star - sha256: 41993fc9e48ad077ad59ee5918ccc59c86628fd3d8ea4d36bd0706e9880ce6df - requires_dist: - - numpy - - rerun-sdk - editable: true - kind: pypi name: rsa version: '4.9' @@ -36805,46 +36448,17 @@ packages: - isort ; extra == 'dev' - black ; extra == 'dev' - mypy ; extra == 'dev' -- kind: pypi - name: segment-anything - version: '1.0' - url: git+https://github.com/facebookresearch/segment-anything.git@dca509fe793f601edb92606367a655c15ac00fdf - requires_dist: - - matplotlib ; extra == 'all' - - pycocotools ; extra == 'all' - - opencv-python ; extra == 'all' - - onnx ; extra == 'all' - - onnxruntime ; extra == 'all' - - flake8 ; extra == 'dev' - - isort ; extra == 'dev' - - black ; extra == 'dev' - - mypy ; extra == 'dev' - kind: pypi name: segment-anything-model version: 0.1.0 path: examples/python/segment_anything_model sha256: 85bc241bedf212c63a39d0251a9dcc0fb3a435087a024d4eafd7f49342a75926 requires_dist: - - segment-anything @ git+https://github.com/facebookresearch/segment-anything.git - numpy - opencv-python - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - - torch==2.2.2 - - torchvision - - tqdm - editable: true -- kind: pypi - name: segment-anything-model - version: 0.1.0 - path: examples/python/segment_anything_model - sha256: 85bc241bedf212c63a39d0251a9dcc0fb3a435087a024d4eafd7f49342a75926 - requires_dist: - segment-anything @ git+https://github.com/facebookresearch/segment-anything.git - - numpy - - opencv-python - - requests>=2.31,<3 - - rerun-sdk - torch==2.2.2 - torchvision - tqdm @@ -37038,14 +36652,6 @@ packages: requires_dist: - rerun-sdk editable: true -- kind: pypi - name: shared-recording - version: 0.1.0 - path: examples/python/shared_recording - sha256: 6f605379e813578a2304663522ed82ab2fd6486cee725b969abd533b5ac8072f - requires_dist: - - rerun-sdk - editable: true - kind: pypi name: shellingham version: 1.5.4 @@ -37060,20 +36666,7 @@ packages: requires_dist: - mesh-to-sdf @ git+https://github.com/marian42/mesh_to_sdf.git - numpy - - requests>=2.31,<3 - - rerun-sdk - - scikit-learn>=1.1.3 - - trimesh==3.15.2 - editable: true -- kind: pypi - name: signed-distance-fields - version: 0.1.0 - path: examples/python/signed_distance_fields - sha256: 32880a8a3883c6aa03c709fe9138ba6b939633562ff98ca27fc22afc3d69f08a - requires_dist: - - mesh-to-sdf @ git+https://github.com/marian42/mesh_to_sdf.git - - numpy - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - scikit-learn>=1.1.3 - trimesh==3.15.2 @@ -37313,14 +36906,6 @@ packages: requires_dist: - rerun-sdk editable: true -- kind: pypi - name: stdio - version: 0.1.0 - path: examples/python/stdio - sha256: 15fb60d3e1c8b7b2d1a4dfcc223bddb267451e8ef7534d42f663d116166d92e2 - requires_dist: - - rerun-sdk - editable: true - kind: pypi name: stringcase version: 1.2.0 @@ -37332,21 +36917,9 @@ packages: path: examples/python/structure_from_motion sha256: b20b79aa7bb2b4225b37d3cb28872a70dc7e9ab2ca9ab138b90d60fc8d7b4c15 requires_dist: - - opencv-python>4.6 - numpy - - requests>=2.31,<3 - - rerun-sdk - - tqdm - editable: true -- kind: pypi - name: structure-from-motion - version: 0.1.0 - path: examples/python/structure_from_motion - sha256: b20b79aa7bb2b4225b37d3cb28872a70dc7e9ab2ca9ab138b90d60fc8d7b4c15 - requires_dist: - opencv-python>4.6 - - numpy - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - tqdm editable: true diff --git a/pixi.toml b/pixi.toml index cebdd5a20fe6..97766b3be9bb 100644 --- a/pixi.toml +++ b/pixi.toml @@ -631,6 +631,8 @@ minimal = { path = "examples/python/minimal", editable = true } minimal_options = { path = "examples/python/minimal_options", editable = true } multiprocess_logging = { path = "examples/python/multiprocess_logging", editable = true } multithreading = { path = "examples/python/multithreading", editable = true } +graph_binary_tree = { path = "examples/python/graph_binary_tree", editable = true } +graph_lattice = { path = "examples/python/graph_lattice", editable = true } nuscenes_dataset = { path = "examples/python/nuscenes_dataset", editable = true } nv12 = { path = "examples/python/nv12", editable = true } objectron = { path = "examples/python/objectron", editable = true } diff --git a/rerun_cpp/src/rerun/archetypes.hpp b/rerun_cpp/src/rerun/archetypes.hpp index 2bb1e89eaac5..74f875d5ac8c 100644 --- a/rerun_cpp/src/rerun/archetypes.hpp +++ b/rerun_cpp/src/rerun/archetypes.hpp @@ -18,6 +18,8 @@ #include "archetypes/encoded_image.hpp" #include "archetypes/geo_line_strings.hpp" #include "archetypes/geo_points.hpp" +#include "archetypes/graph_edges.hpp" +#include "archetypes/graph_nodes.hpp" #include "archetypes/image.hpp" #include "archetypes/instance_poses3d.hpp" #include "archetypes/line_strips2d.hpp" diff --git a/rerun_cpp/src/rerun/archetypes/.gitattributes b/rerun_cpp/src/rerun/archetypes/.gitattributes index 979a5d48e652..8b89b32e2722 100644 --- a/rerun_cpp/src/rerun/archetypes/.gitattributes +++ b/rerun_cpp/src/rerun/archetypes/.gitattributes @@ -33,6 +33,10 @@ geo_line_strings.cpp linguist-generated=true geo_line_strings.hpp linguist-generated=true geo_points.cpp linguist-generated=true geo_points.hpp linguist-generated=true +graph_edges.cpp linguist-generated=true +graph_edges.hpp linguist-generated=true +graph_nodes.cpp linguist-generated=true +graph_nodes.hpp linguist-generated=true image.cpp linguist-generated=true image.hpp linguist-generated=true instance_poses3d.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/archetypes/graph_edges.cpp b/rerun_cpp/src/rerun/archetypes/graph_edges.cpp new file mode 100644 index 000000000000..92b09a1b20f6 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/graph_edges.cpp @@ -0,0 +1,38 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs". + +#include "graph_edges.hpp" + +#include "../collection_adapter_builtins.hpp" + +namespace rerun::archetypes {} + +namespace rerun { + + Result> AsComponents::serialize( + const archetypes::GraphEdges& archetype + ) { + using namespace archetypes; + std::vector cells; + cells.reserve(3); + + { + auto result = ComponentBatch::from_loggable(archetype.edges); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.graph_type.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.graph_type.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + { + auto indicator = GraphEdges::IndicatorComponent(); + auto result = ComponentBatch::from_loggable(indicator); + RR_RETURN_NOT_OK(result.error); + cells.emplace_back(std::move(result.value)); + } + + return cells; + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/graph_edges.hpp b/rerun_cpp/src/rerun/archetypes/graph_edges.hpp new file mode 100644 index 000000000000..65863e21c154 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/graph_edges.hpp @@ -0,0 +1,72 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs". + +#pragma once + +#include "../collection.hpp" +#include "../compiler_utils.hpp" +#include "../component_batch.hpp" +#include "../components/graph_edge.hpp" +#include "../components/graph_type.hpp" +#include "../indicator_component.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::archetypes { + /// **Archetype**: A list of edges in a graph. + /// + /// By default, edges are undirected. + /// + /// ⚠ **This is an experimental API! It is not fully supported, and is likely to change significantly in future versions.** + struct GraphEdges { + /// A list of node tuples. + Collection edges; + + /// Specifies if the graph is directed or undirected. + /// + /// If no `GraphType` is provided, the graph is assumed to be undirected. + std::optional graph_type; + + public: + static constexpr const char IndicatorComponentName[] = + "rerun.components.GraphEdgesIndicator"; + + /// Indicator component, used to identify the archetype when converting to a list of components. + using IndicatorComponent = rerun::components::IndicatorComponent; + + public: + GraphEdges() = default; + GraphEdges(GraphEdges&& other) = default; + + explicit GraphEdges(Collection _edges) + : edges(std::move(_edges)) {} + + /// Specifies if the graph is directed or undirected. + /// + /// If no `GraphType` is provided, the graph is assumed to be undirected. + GraphEdges with_graph_type(rerun::components::GraphType _graph_type) && { + graph_type = std::move(_graph_type); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + }; + +} // namespace rerun::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> serialize(const archetypes::GraphEdges& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/graph_nodes.cpp b/rerun_cpp/src/rerun/archetypes/graph_nodes.cpp new file mode 100644 index 000000000000..10faaab54df0 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/graph_nodes.cpp @@ -0,0 +1,58 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs". + +#include "graph_nodes.hpp" + +#include "../collection_adapter_builtins.hpp" + +namespace rerun::archetypes {} + +namespace rerun { + + Result> AsComponents::serialize( + const archetypes::GraphNodes& archetype + ) { + using namespace archetypes; + std::vector cells; + cells.reserve(7); + + { + auto result = ComponentBatch::from_loggable(archetype.node_ids); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.positions.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.positions.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.colors.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.colors.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.labels.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.labels.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.show_labels.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.show_labels.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + if (archetype.radii.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.radii.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } + { + auto indicator = GraphNodes::IndicatorComponent(); + auto result = ComponentBatch::from_loggable(indicator); + RR_RETURN_NOT_OK(result.error); + cells.emplace_back(std::move(result.value)); + } + + return cells; + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp b/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp new file mode 100644 index 000000000000..1323569a7137 --- /dev/null +++ b/rerun_cpp/src/rerun/archetypes/graph_nodes.hpp @@ -0,0 +1,110 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs". + +#pragma once + +#include "../collection.hpp" +#include "../compiler_utils.hpp" +#include "../component_batch.hpp" +#include "../components/color.hpp" +#include "../components/graph_node.hpp" +#include "../components/position2d.hpp" +#include "../components/radius.hpp" +#include "../components/show_labels.hpp" +#include "../components/text.hpp" +#include "../indicator_component.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::archetypes { + /// **Archetype**: A list of nodes in a graph with optional labels, colors, etc. + /// + /// ⚠ **This is an experimental API! It is not fully supported, and is likely to change significantly in future versions.** + struct GraphNodes { + /// A list of node IDs. + Collection node_ids; + + /// Optional center positions of the nodes. + std::optional> positions; + + /// Optional colors for the boxes. + std::optional> colors; + + /// Optional text labels for the node. + std::optional> labels; + + /// Optional choice of whether the text labels should be shown by default. + std::optional show_labels; + + /// Optional radii for nodes. + std::optional> radii; + + public: + static constexpr const char IndicatorComponentName[] = + "rerun.components.GraphNodesIndicator"; + + /// Indicator component, used to identify the archetype when converting to a list of components. + using IndicatorComponent = rerun::components::IndicatorComponent; + + public: + GraphNodes() = default; + GraphNodes(GraphNodes&& other) = default; + + explicit GraphNodes(Collection _node_ids) + : node_ids(std::move(_node_ids)) {} + + /// Optional center positions of the nodes. + GraphNodes with_positions(Collection _positions) && { + positions = std::move(_positions); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + + /// Optional colors for the boxes. + GraphNodes with_colors(Collection _colors) && { + colors = std::move(_colors); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + + /// Optional text labels for the node. + GraphNodes with_labels(Collection _labels) && { + labels = std::move(_labels); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + + /// Optional choice of whether the text labels should be shown by default. + GraphNodes with_show_labels(rerun::components::ShowLabels _show_labels) && { + show_labels = std::move(_show_labels); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + + /// Optional radii for nodes. + GraphNodes with_radii(Collection _radii) && { + radii = std::move(_radii); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + }; + +} // namespace rerun::archetypes + +namespace rerun { + /// \private + template + struct AsComponents; + + /// \private + template <> + struct AsComponents { + /// Serialize all set component batches. + static Result> serialize(const archetypes::GraphNodes& archetype + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components.hpp b/rerun_cpp/src/rerun/components.hpp index 8806bc1c90f5..fd37481c6293 100644 --- a/rerun_cpp/src/rerun/components.hpp +++ b/rerun_cpp/src/rerun/components.hpp @@ -19,6 +19,9 @@ #include "components/fill_ratio.hpp" #include "components/gamma_correction.hpp" #include "components/geo_line_string.hpp" +#include "components/graph_edge.hpp" +#include "components/graph_node.hpp" +#include "components/graph_type.hpp" #include "components/half_size2d.hpp" #include "components/half_size3d.hpp" #include "components/image_buffer.hpp" diff --git a/rerun_cpp/src/rerun/components/.gitattributes b/rerun_cpp/src/rerun/components/.gitattributes index 7fb37fd6dd9b..011cb60f9609 100644 --- a/rerun_cpp/src/rerun/components/.gitattributes +++ b/rerun_cpp/src/rerun/components/.gitattributes @@ -23,6 +23,10 @@ fill_ratio.hpp linguist-generated=true gamma_correction.hpp linguist-generated=true geo_line_string.cpp linguist-generated=true geo_line_string.hpp linguist-generated=true +graph_edge.hpp linguist-generated=true +graph_node.hpp linguist-generated=true +graph_type.cpp linguist-generated=true +graph_type.hpp linguist-generated=true half_size2d.hpp linguist-generated=true half_size3d.hpp linguist-generated=true image_buffer.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/components/graph_edge.hpp b/rerun_cpp/src/rerun/components/graph_edge.hpp new file mode 100644 index 000000000000..30fea4203382 --- /dev/null +++ b/rerun_cpp/src/rerun/components/graph_edge.hpp @@ -0,0 +1,74 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_edge.fbs". + +#pragma once + +#include "../datatypes/utf8pair.hpp" +#include "../result.hpp" + +#include +#include +#include + +namespace rerun::components { + /// **Component**: An edge in a graph connecting two nodes. + struct GraphEdge { + rerun::datatypes::Utf8Pair edge; + + public: // START of extensions from graph_edge_ext.cpp: + /// Create a new graph edge from a pair of strings. + GraphEdge(rerun::datatypes::Utf8 first_, rerun::datatypes::Utf8 second_) + : edge(std::move(first_), std::move(second_)) {} + + // END of extensions from graph_edge_ext.cpp, start of generated code: + + public: + GraphEdge() = default; + + GraphEdge(rerun::datatypes::Utf8Pair edge_) : edge(std::move(edge_)) {} + + GraphEdge& operator=(rerun::datatypes::Utf8Pair edge_) { + edge = std::move(edge_); + return *this; + } + + /// Cast to the underlying Utf8Pair datatype + operator rerun::datatypes::Utf8Pair() const { + return edge; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Utf8Pair) == sizeof(components::GraphEdge)); + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.components.GraphEdge"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::GraphEdge` into an arrow array. + static Result> to_arrow( + const components::GraphEdge* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->edge, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/graph_edge_ext.cpp b/rerun_cpp/src/rerun/components/graph_edge_ext.cpp new file mode 100644 index 000000000000..8ffe5b0c68cf --- /dev/null +++ b/rerun_cpp/src/rerun/components/graph_edge_ext.cpp @@ -0,0 +1,20 @@ +//#define EDIT_EXTENSION + +#ifdef EDIT_EXTENSION +#include "graph_edge.hpp" + +namespace rerun { + namespace components { + + // + + /// Create a new graph edge from a pair of strings. + GraphEdge(rerun::datatypes::Utf8 first_, rerun::datatypes::Utf8 second_) + : edge(std::move(first_), std::move(second_)) {} + + // + + } // namespace components +} // namespace rerun + +#endif diff --git a/rerun_cpp/src/rerun/components/graph_node.hpp b/rerun_cpp/src/rerun/components/graph_node.hpp new file mode 100644 index 000000000000..53ce179ecbe4 --- /dev/null +++ b/rerun_cpp/src/rerun/components/graph_node.hpp @@ -0,0 +1,78 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_node.fbs". + +#pragma once + +#include "../datatypes/utf8.hpp" +#include "../result.hpp" + +#include +#include +#include +#include + +namespace rerun::components { + /// **Component**: A string-based ID representing a node in a graph. + struct GraphNode { + rerun::datatypes::Utf8 id; + + public: // START of extensions from graph_node_ext.cpp: + /// Create a new graph edge from a c string. + GraphNode(const char* value_) : id(value_) {} + + // END of extensions from graph_node_ext.cpp, start of generated code: + + public: + GraphNode() = default; + + GraphNode(rerun::datatypes::Utf8 id_) : id(std::move(id_)) {} + + GraphNode& operator=(rerun::datatypes::Utf8 id_) { + id = std::move(id_); + return *this; + } + + GraphNode(std::string value_) : id(std::move(value_)) {} + + GraphNode& operator=(std::string value_) { + id = std::move(value_); + return *this; + } + + /// Cast to the underlying Utf8 datatype + operator rerun::datatypes::Utf8() const { + return id; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Utf8) == sizeof(components::GraphNode)); + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.components.GraphNode"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::GraphNode` into an arrow array. + static Result> to_arrow( + const components::GraphNode* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow(&instances->id, num_instances); + } + } + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/graph_node_ext.cpp b/rerun_cpp/src/rerun/components/graph_node_ext.cpp new file mode 100644 index 000000000000..fa63e70b6834 --- /dev/null +++ b/rerun_cpp/src/rerun/components/graph_node_ext.cpp @@ -0,0 +1,19 @@ +//#define EDIT_EXTENSION + +#ifdef EDIT_EXTENSION +#include "graph_node.hpp" + +namespace rerun { + namespace components { + + // + + /// Create a new graph edge from a c string. + GraphNode(const char* value_) : id(value_) {} + + // + + } // namespace components +} // namespace rerun + +#endif diff --git a/rerun_cpp/src/rerun/components/graph_type.cpp b/rerun_cpp/src/rerun/components/graph_type.cpp new file mode 100644 index 000000000000..e7d3390016f5 --- /dev/null +++ b/rerun_cpp/src/rerun/components/graph_type.cpp @@ -0,0 +1,56 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_type.fbs". + +#include "graph_type.hpp" + +#include +#include + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype() { + static const auto datatype = arrow::uint8(); + return datatype; + } + + Result> Loggable::to_arrow( + const components::GraphType* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::UInt8Builder* builder, const components::GraphType* elements, size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + ARROW_RETURN_NOT_OK(builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + const auto variant = elements[elem_idx]; + ARROW_RETURN_NOT_OK(builder->Append(static_cast(variant))); + } + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/components/graph_type.hpp b/rerun_cpp/src/rerun/components/graph_type.hpp new file mode 100644 index 000000000000..6a1b7a09312e --- /dev/null +++ b/rerun_cpp/src/rerun/components/graph_type.hpp @@ -0,0 +1,56 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/components/graph_type.fbs". + +#pragma once + +#include "../result.hpp" + +#include +#include + +namespace arrow { + /// \private + template + class NumericBuilder; + + class Array; + class DataType; + class UInt8Type; + using UInt8Builder = NumericBuilder; +} // namespace arrow + +namespace rerun::components { + /// **Component**: Specifies if a graph has directed or undirected edges. + enum class GraphType : uint8_t { + + /// The graph has undirected edges. + Undirected = 1, + + /// The graph has directed edges. + Directed = 2, + }; +} // namespace rerun::components + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.components.GraphType"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::components::GraphType` into an arrow array. + static Result> to_arrow( + const components::GraphType* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::UInt8Builder* builder, const components::GraphType* elements, size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes.hpp b/rerun_cpp/src/rerun/datatypes.hpp index e1ebcff3a151..e18b3815ecec 100644 --- a/rerun_cpp/src/rerun/datatypes.hpp +++ b/rerun_cpp/src/rerun/datatypes.hpp @@ -38,6 +38,7 @@ #include "datatypes/uint32.hpp" #include "datatypes/uint64.hpp" #include "datatypes/utf8.hpp" +#include "datatypes/utf8pair.hpp" #include "datatypes/uuid.hpp" #include "datatypes/uvec2d.hpp" #include "datatypes/uvec3d.hpp" diff --git a/rerun_cpp/src/rerun/datatypes/.gitattributes b/rerun_cpp/src/rerun/datatypes/.gitattributes index 133573f66723..fea5857aed33 100644 --- a/rerun_cpp/src/rerun/datatypes/.gitattributes +++ b/rerun_cpp/src/rerun/datatypes/.gitattributes @@ -73,6 +73,8 @@ uint64.cpp linguist-generated=true uint64.hpp linguist-generated=true utf8.cpp linguist-generated=true utf8.hpp linguist-generated=true +utf8pair.cpp linguist-generated=true +utf8pair.hpp linguist-generated=true uuid.cpp linguist-generated=true uuid.hpp linguist-generated=true uvec2d.cpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/datatypes/tensor_dimension_ext.cpp b/rerun_cpp/src/rerun/datatypes/tensor_dimension_ext.cpp index 1e1d8ce97a49..6699700c5ae9 100644 --- a/rerun_cpp/src/rerun/datatypes/tensor_dimension_ext.cpp +++ b/rerun_cpp/src/rerun/datatypes/tensor_dimension_ext.cpp @@ -4,7 +4,7 @@ // #define EDIT_EXTENSION namespace rerun { - namespace archetypes { + namespace datatypes { #ifdef EDIT_EXTENSION // @@ -17,5 +17,5 @@ namespace rerun { // #endif - } // namespace archetypes + } // namespace datatypes } // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/utf8.hpp b/rerun_cpp/src/rerun/datatypes/utf8.hpp index b321ed5f360d..4672e273b684 100644 --- a/rerun_cpp/src/rerun/datatypes/utf8.hpp +++ b/rerun_cpp/src/rerun/datatypes/utf8.hpp @@ -22,6 +22,16 @@ namespace rerun::datatypes { std::string value; public: // START of extensions from utf8_ext.cpp: + /// Construct from a C string. + Utf8(const char* utf8_) : value(utf8_) {} + + /// Explicit copy assignment from a C string to avoid ambiguity in some cases. + Utf8& operator=(const char* utf8_) { + value = utf8_; + return *this; + } + + /// Returns a pointer to the underlying C string. const char* c_str() const { return value.c_str(); } diff --git a/rerun_cpp/src/rerun/datatypes/utf8_ext.cpp b/rerun_cpp/src/rerun/datatypes/utf8_ext.cpp index 0ae735b4e7cc..c484017851a0 100644 --- a/rerun_cpp/src/rerun/datatypes/utf8_ext.cpp +++ b/rerun_cpp/src/rerun/datatypes/utf8_ext.cpp @@ -15,6 +15,16 @@ namespace rerun { // + /// Construct from a C string. + Utf8(const char* utf8_) : value(utf8_) {} + + /// Explicit copy assignment from a C string to avoid ambiguity in some cases. + Utf8& operator=(const char* utf8_) { + value = utf8_; + return *this; + } + + /// Returns a pointer to the underlying C string. const char* c_str() const { return value.c_str(); } diff --git a/rerun_cpp/src/rerun/datatypes/utf8pair.cpp b/rerun_cpp/src/rerun/datatypes/utf8pair.cpp new file mode 100644 index 000000000000..2014e39616e5 --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/utf8pair.cpp @@ -0,0 +1,81 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs". + +#include "utf8pair.hpp" + +#include "utf8.hpp" + +#include +#include + +namespace rerun::datatypes {} + +namespace rerun { + const std::shared_ptr& Loggable::arrow_datatype() { + static const auto datatype = arrow::struct_({ + arrow::field("first", Loggable::arrow_datatype(), false), + arrow::field("second", Loggable::arrow_datatype(), false), + }); + return datatype; + } + + Result> Loggable::to_arrow( + const datatypes::Utf8Pair* instances, size_t num_instances + ) { + // TODO(andreas): Allow configuring the memory pool. + arrow::MemoryPool* pool = arrow::default_memory_pool(); + auto datatype = arrow_datatype(); + + ARROW_ASSIGN_OR_RAISE(auto builder, arrow::MakeBuilder(datatype, pool)) + if (instances && num_instances > 0) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + static_cast(builder.get()), + instances, + num_instances + )); + } + std::shared_ptr array; + ARROW_RETURN_NOT_OK(builder->Finish(&array)); + return array; + } + + rerun::Error Loggable::fill_arrow_array_builder( + arrow::StructBuilder* builder, const datatypes::Utf8Pair* elements, size_t num_elements + ) { + if (builder == nullptr) { + return rerun::Error(ErrorCode::UnexpectedNullArgument, "Passed array builder is null."); + } + if (elements == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Cannot serialize null pointer to arrow array." + ); + } + + { + auto field_builder = static_cast(builder->field_builder(0)); + ARROW_RETURN_NOT_OK(field_builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + field_builder, + &elements[elem_idx].first, + 1 + )); + } + } + { + auto field_builder = static_cast(builder->field_builder(1)); + ARROW_RETURN_NOT_OK(field_builder->Reserve(static_cast(num_elements))); + for (size_t elem_idx = 0; elem_idx < num_elements; elem_idx += 1) { + RR_RETURN_NOT_OK(Loggable::fill_arrow_array_builder( + field_builder, + &elements[elem_idx].second, + 1 + )); + } + } + ARROW_RETURN_NOT_OK(builder->AppendValues(static_cast(num_elements), nullptr)); + + return Error::ok(); + } +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/utf8pair.hpp b/rerun_cpp/src/rerun/datatypes/utf8pair.hpp new file mode 100644 index 000000000000..fc9137a4d545 --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/utf8pair.hpp @@ -0,0 +1,61 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs". + +#pragma once + +#include "../result.hpp" +#include "utf8.hpp" + +#include +#include + +namespace arrow { + class Array; + class DataType; + class StructBuilder; +} // namespace arrow + +namespace rerun::datatypes { + /// **Datatype**: Stores a tuple of UTF-8 strings. + struct Utf8Pair { + /// The first string. + rerun::datatypes::Utf8 first; + + /// The second string. + rerun::datatypes::Utf8 second; + + public: // START of extensions from utf8pair_ext.cpp: + /// Creates a string pair. + Utf8Pair(rerun::datatypes::Utf8 first_, rerun::datatypes::Utf8 second_) + : first(std::move(first_)), second(std::move(second_)) {} + + // END of extensions from utf8pair_ext.cpp, start of generated code: + + public: + Utf8Pair() = default; + }; +} // namespace rerun::datatypes + +namespace rerun { + template + struct Loggable; + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.datatypes.Utf8Pair"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype(); + + /// Serializes an array of `rerun::datatypes::Utf8Pair` into an arrow array. + static Result> to_arrow( + const datatypes::Utf8Pair* instances, size_t num_instances + ); + + /// Fills an arrow array builder with an array of this type. + static rerun::Error fill_arrow_array_builder( + arrow::StructBuilder* builder, const datatypes::Utf8Pair* elements, size_t num_elements + ); + }; +} // namespace rerun diff --git a/rerun_cpp/src/rerun/datatypes/utf8pair_ext.cpp b/rerun_cpp/src/rerun/datatypes/utf8pair_ext.cpp new file mode 100644 index 000000000000..db882494fd41 --- /dev/null +++ b/rerun_cpp/src/rerun/datatypes/utf8pair_ext.cpp @@ -0,0 +1,19 @@ +#include +#include "utf8pair.hpp" + +// #define EDIT_EXTENSION + +namespace rerun { + namespace datatypes { + +#ifdef EDIT_EXTENSION + // + + /// Creates a string pair. + Utf8Pair(rerun::datatypes::Utf8 first_, rerun::datatypes::Utf8 second_) + : first(std::move(first_)), second(std::move(second_)) {} + + // +#endif + } // namespace datatypes +} // namespace rerun diff --git a/rerun_notebook/package-lock.json b/rerun_notebook/package-lock.json index 98ffe5ec105e..587b02e1a0d8 100644 --- a/rerun_notebook/package-lock.json +++ b/rerun_notebook/package-lock.json @@ -17,7 +17,7 @@ }, "../rerun_js/web-viewer": { "name": "@rerun-io/web-viewer", - "version": "0.20.1-alpha.2", + "version": "0.21.0-alpha.1+dev", "license": "MIT", "devDependencies": { "dts-buddy": "^0.3.0", diff --git a/rerun_py/docs/gen_common_index.py b/rerun_py/docs/gen_common_index.py index 8b8f8b863c8c..fe3d68ef05ca 100755 --- a/rerun_py/docs/gen_common_index.py +++ b/rerun_py/docs/gen_common_index.py @@ -221,6 +221,14 @@ class Section: ], gen_page=False, ), + Section( + title="Graphs", + class_list=[ + "archetypes.GraphNodes", + "archetypes.GraphEdges", + ], + gen_page=False, + ), Section( title="Tensors", class_list=["archetypes.Tensor"], diff --git a/rerun_py/rerun_sdk/rerun/__init__.py b/rerun_py/rerun_sdk/rerun/__init__.py index bd1ebe50f2a6..2003fa3f4d9c 100644 --- a/rerun_py/rerun_sdk/rerun/__init__.py +++ b/rerun_py/rerun_sdk/rerun/__init__.py @@ -77,6 +77,8 @@ EncodedImage as EncodedImage, GeoLineStrings as GeoLineStrings, GeoPoints as GeoPoints, + GraphEdges as GraphEdges, + GraphNodes as GraphNodes, Image as Image, InstancePoses3D as InstancePoses3D, LineStrips2D as LineStrips2D, @@ -104,6 +106,8 @@ ) from .components import ( AlbedoFactor as AlbedoFactor, + GraphEdge as GraphEdge, + GraphType as GraphType, MediaType as MediaType, Radius as Radius, Scale3D as Scale3D, diff --git a/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes b/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes index e7a8ba7d6a2f..054ebcd0186d 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/archetypes/.gitattributes @@ -18,6 +18,8 @@ ellipsoids3d.py linguist-generated=true encoded_image.py linguist-generated=true geo_line_strings.py linguist-generated=true geo_points.py linguist-generated=true +graph_edges.py linguist-generated=true +graph_nodes.py linguist-generated=true image.py linguist-generated=true instance_poses3d.py linguist-generated=true line_strips2d.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/archetypes/__init__.py b/rerun_py/rerun_sdk/rerun/archetypes/__init__.py index 0324104dc218..12b8f2c904d1 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/__init__.py @@ -18,6 +18,8 @@ from .encoded_image import EncodedImage from .geo_line_strings import GeoLineStrings from .geo_points import GeoPoints +from .graph_edges import GraphEdges +from .graph_nodes import GraphNodes from .image import Image from .instance_poses3d import InstancePoses3D from .line_strips2d import LineStrips2D @@ -54,6 +56,8 @@ "EncodedImage", "GeoLineStrings", "GeoPoints", + "GraphEdges", + "GraphNodes", "Image", "InstancePoses3D", "LineStrips2D", diff --git a/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py b/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py new file mode 100644 index 000000000000..efd87dbb8f19 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/archetypes/graph_edges.py @@ -0,0 +1,86 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/archetypes/graph_edges.fbs". + +# You can extend this class by creating a "GraphEdgesExt" class in "graph_edges_ext.py". + +from __future__ import annotations + +from typing import Any + +from attrs import define, field + +from .. import components, datatypes +from .._baseclasses import ( + Archetype, +) +from ..error_utils import catch_and_log_exceptions + +__all__ = ["GraphEdges"] + + +@define(str=False, repr=False, init=False) +class GraphEdges(Archetype): + """ + **Archetype**: A list of edges in a graph. + + By default, edges are undirected. + + ⚠️ **This is an experimental API! It is not fully supported, and is likely to change significantly in future versions.** + """ + + def __init__(self: Any, edges: datatypes.Utf8PairArrayLike, *, graph_type: components.GraphTypeLike | None = None): + """ + Create a new instance of the GraphEdges archetype. + + Parameters + ---------- + edges: + A list of node tuples. + graph_type: + Specifies if the graph is directed or undirected. + + If no `GraphType` is provided, the graph is assumed to be undirected. + + """ + + # You can define your own __init__ function as a member of GraphEdgesExt in graph_edges_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__(edges=edges, graph_type=graph_type) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + edges=None, # type: ignore[arg-type] + graph_type=None, # type: ignore[arg-type] + ) + + @classmethod + def _clear(cls) -> GraphEdges: + """Produce an empty GraphEdges, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + edges: components.GraphEdgeBatch = field( + metadata={"component": "required"}, + converter=components.GraphEdgeBatch._required, # type: ignore[misc] + ) + # A list of node tuples. + # + # (Docstring intentionally commented out to hide this field from the docs) + + graph_type: components.GraphTypeBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.GraphTypeBatch._optional, # type: ignore[misc] + ) + # Specifies if the graph is directed or undirected. + # + # If no `GraphType` is provided, the graph is assumed to be undirected. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py b/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py new file mode 100644 index 000000000000..bed4372bbaad --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/archetypes/graph_nodes.py @@ -0,0 +1,144 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/archetypes/graph_nodes.fbs". + +# You can extend this class by creating a "GraphNodesExt" class in "graph_nodes_ext.py". + +from __future__ import annotations + +from typing import Any + +from attrs import define, field + +from .. import components, datatypes +from .._baseclasses import ( + Archetype, +) +from ..error_utils import catch_and_log_exceptions + +__all__ = ["GraphNodes"] + + +@define(str=False, repr=False, init=False) +class GraphNodes(Archetype): + """ + **Archetype**: A list of nodes in a graph with optional labels, colors, etc. + + ⚠️ **This is an experimental API! It is not fully supported, and is likely to change significantly in future versions.** + """ + + def __init__( + self: Any, + node_ids: datatypes.Utf8ArrayLike, + *, + positions: datatypes.Vec2DArrayLike | None = None, + colors: datatypes.Rgba32ArrayLike | None = None, + labels: datatypes.Utf8ArrayLike | None = None, + show_labels: datatypes.BoolLike | None = None, + radii: datatypes.Float32ArrayLike | None = None, + ): + """ + Create a new instance of the GraphNodes archetype. + + Parameters + ---------- + node_ids: + A list of node IDs. + positions: + Optional center positions of the nodes. + colors: + Optional colors for the boxes. + labels: + Optional text labels for the node. + show_labels: + Optional choice of whether the text labels should be shown by default. + radii: + Optional radii for nodes. + + """ + + # You can define your own __init__ function as a member of GraphNodesExt in graph_nodes_ext.py + with catch_and_log_exceptions(context=self.__class__.__name__): + self.__attrs_init__( + node_ids=node_ids, + positions=positions, + colors=colors, + labels=labels, + show_labels=show_labels, + radii=radii, + ) + return + self.__attrs_clear__() + + def __attrs_clear__(self) -> None: + """Convenience method for calling `__attrs_init__` with all `None`s.""" + self.__attrs_init__( + node_ids=None, # type: ignore[arg-type] + positions=None, # type: ignore[arg-type] + colors=None, # type: ignore[arg-type] + labels=None, # type: ignore[arg-type] + show_labels=None, # type: ignore[arg-type] + radii=None, # type: ignore[arg-type] + ) + + @classmethod + def _clear(cls) -> GraphNodes: + """Produce an empty GraphNodes, bypassing `__init__`.""" + inst = cls.__new__(cls) + inst.__attrs_clear__() + return inst + + node_ids: components.GraphNodeBatch = field( + metadata={"component": "required"}, + converter=components.GraphNodeBatch._required, # type: ignore[misc] + ) + # A list of node IDs. + # + # (Docstring intentionally commented out to hide this field from the docs) + + positions: components.Position2DBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.Position2DBatch._optional, # type: ignore[misc] + ) + # Optional center positions of the nodes. + # + # (Docstring intentionally commented out to hide this field from the docs) + + colors: components.ColorBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.ColorBatch._optional, # type: ignore[misc] + ) + # Optional colors for the boxes. + # + # (Docstring intentionally commented out to hide this field from the docs) + + labels: components.TextBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.TextBatch._optional, # type: ignore[misc] + ) + # Optional text labels for the node. + # + # (Docstring intentionally commented out to hide this field from the docs) + + show_labels: components.ShowLabelsBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.ShowLabelsBatch._optional, # type: ignore[misc] + ) + # Optional choice of whether the text labels should be shown by default. + # + # (Docstring intentionally commented out to hide this field from the docs) + + radii: components.RadiusBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.RadiusBatch._optional, # type: ignore[misc] + ) + # Optional radii for nodes. + # + # (Docstring intentionally commented out to hide this field from the docs) + + __str__ = Archetype.__str__ + __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/blueprint/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/__init__.py index 888ea3520ecd..277099a55d73 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/__init__.py @@ -53,6 +53,7 @@ from .views import ( BarChartView as BarChartView, DataframeView as DataframeView, + GraphView as GraphView, MapView as MapView, Spatial2DView as Spatial2DView, Spatial3DView as Spatial3DView, diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes b/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes index b95c2652ca07..e1cc8939daa2 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/.gitattributes @@ -4,6 +4,7 @@ __init__.py linguist-generated=true bar_chart_view.py linguist-generated=true dataframe_view.py linguist-generated=true +graph_view.py linguist-generated=true map_view.py linguist-generated=true spatial2d_view.py linguist-generated=true spatial3d_view.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py b/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py index 992739f85bd3..2e03a2700165 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/__init__.py @@ -4,6 +4,7 @@ from .bar_chart_view import BarChartView from .dataframe_view import DataframeView +from .graph_view import GraphView from .map_view import MapView from .spatial2d_view import Spatial2DView from .spatial3d_view import Spatial3DView @@ -15,6 +16,7 @@ __all__ = [ "BarChartView", "DataframeView", + "GraphView", "MapView", "Spatial2DView", "Spatial3DView", diff --git a/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py b/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py new file mode 100644 index 000000000000..4c43bc8e055d --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/blueprint/views/graph_view.py @@ -0,0 +1,125 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/blueprint/views/graph.fbs". + +from __future__ import annotations + +from typing import Union + +__all__ = ["GraphView"] + + +from ... import datatypes +from ..._baseclasses import AsComponents, ComponentBatchLike +from ...datatypes import EntityPathLike, Utf8Like +from .. import archetypes as blueprint_archetypes +from ..api import SpaceView, SpaceViewContentsLike + + +class GraphView(SpaceView): + """ + **View**: A graph view to display time-variying, directed or undirected graph visualization. + + Example + ------- + ### Use a blueprint to create a graph view.: + ```python + import rerun as rr + import rerun.blueprint as rrb + + rr.init("rerun_example_graph_view", spawn=True) + + rr.log( + "simple", + rr.GraphNodes( + node_ids=["a", "b", "c"], positions=[(0.0, 100.0), (-100.0, 0.0), (100.0, 0.0)], labels=["A", "B", "C"] + ), + ) + + # Create a Spatial2D view to display the points. + blueprint = rrb.Blueprint( + rrb.GraphView( + origin="/", + name="Graph", + # Note that this translates the viewbox. + visual_bounds=rrb.VisualBounds2D(x_range=[-150, 150], y_range=[-50, 150]), + ), + collapse_panels=True, + ) + + rr.send_blueprint(blueprint) + ``` +
+ + + + + + + +
+ + """ + + def __init__( + self, + *, + origin: EntityPathLike = "/", + contents: SpaceViewContentsLike = "$origin/**", + name: Utf8Like | None = None, + visible: datatypes.BoolLike | None = None, + defaults: list[Union[AsComponents, ComponentBatchLike]] = [], + overrides: dict[EntityPathLike, list[ComponentBatchLike]] = {}, + visual_bounds: blueprint_archetypes.VisualBounds2D | None = None, + ) -> None: + """ + Construct a blueprint for a new GraphView view. + + Parameters + ---------- + origin: + The `EntityPath` to use as the origin of this view. + All other entities will be transformed to be displayed relative to this origin. + contents: + The contents of the view specified as a query expression. + This is either a single expression, or a list of multiple expressions. + See [rerun.blueprint.archetypes.SpaceViewContents][]. + name: + The display name of the view. + visible: + Whether this view is visible. + + Defaults to true if not specified. + defaults: + List of default components or component batches to add to the space view. When an archetype + in the view is missing a component included in this set, the value of default will be used + instead of the normal fallback for the visualizer. + overrides: + Dictionary of overrides to apply to the space view. The key is the path to the entity where the override + should be applied. The value is a list of component or component batches to apply to the entity. + + Important note: the path must be a fully qualified entity path starting at the root. The override paths + do not yet support `$origin` relative paths or glob expressions. + This will be addressed in . + visual_bounds: + Everything within these bounds is guaranteed to be visible. + + Somethings outside of these bounds may also be visible due to letterboxing. + + """ + + properties: dict[str, AsComponents] = {} + if visual_bounds is not None: + if not isinstance(visual_bounds, blueprint_archetypes.VisualBounds2D): + visual_bounds = blueprint_archetypes.VisualBounds2D(visual_bounds) + properties["VisualBounds2D"] = visual_bounds + + super().__init__( + class_identifier="Graph", + origin=origin, + contents=contents, + name=name, + visible=visible, + properties=properties, + defaults=defaults, + overrides=overrides, + ) diff --git a/rerun_py/rerun_sdk/rerun/components/.gitattributes b/rerun_py/rerun_sdk/rerun/components/.gitattributes index b27be864833b..8f860f91817f 100644 --- a/rerun_py/rerun_sdk/rerun/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/components/.gitattributes @@ -19,6 +19,9 @@ fill_mode.py linguist-generated=true fill_ratio.py linguist-generated=true gamma_correction.py linguist-generated=true geo_line_string.py linguist-generated=true +graph_edge.py linguist-generated=true +graph_node.py linguist-generated=true +graph_type.py linguist-generated=true half_size2d.py linguist-generated=true half_size3d.py linguist-generated=true image_buffer.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/components/__init__.py b/rerun_py/rerun_sdk/rerun/components/__init__.py index 5226140b1b9e..10ec22321fd2 100644 --- a/rerun_py/rerun_sdk/rerun/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/components/__init__.py @@ -29,6 +29,9 @@ from .fill_ratio import FillRatio, FillRatioBatch from .gamma_correction import GammaCorrection, GammaCorrectionBatch from .geo_line_string import GeoLineString, GeoLineStringArrayLike, GeoLineStringBatch, GeoLineStringLike +from .graph_edge import GraphEdge, GraphEdgeBatch +from .graph_node import GraphNode, GraphNodeBatch +from .graph_type import GraphType, GraphTypeArrayLike, GraphTypeBatch, GraphTypeLike from .half_size2d import HalfSize2D, HalfSize2DBatch from .half_size3d import HalfSize3D, HalfSize3DBatch from .image_buffer import ImageBuffer, ImageBufferBatch @@ -134,6 +137,14 @@ "GeoLineStringArrayLike", "GeoLineStringBatch", "GeoLineStringLike", + "GraphEdge", + "GraphEdgeBatch", + "GraphNode", + "GraphNodeBatch", + "GraphType", + "GraphTypeArrayLike", + "GraphTypeBatch", + "GraphTypeLike", "HalfSize2D", "HalfSize2DBatch", "HalfSize3D", diff --git a/rerun_py/rerun_sdk/rerun/components/graph_edge.py b/rerun_py/rerun_sdk/rerun/components/graph_edge.py new file mode 100644 index 000000000000..64d597eb4ec5 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/graph_edge.py @@ -0,0 +1,32 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/components/graph_edge.fbs". + +# You can extend this class by creating a "GraphEdgeExt" class in "graph_edge_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["GraphEdge", "GraphEdgeBatch"] + + +class GraphEdge(datatypes.Utf8Pair, ComponentMixin): + """**Component**: An edge in a graph connecting two nodes.""" + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of GraphEdgeExt in graph_edge_ext.py + + # Note: there are no fields here because GraphEdge delegates to datatypes.Utf8Pair + pass + + +class GraphEdgeBatch(datatypes.Utf8PairBatch, ComponentBatchMixin): + _COMPONENT_NAME: str = "rerun.components.GraphEdge" + + +# This is patched in late to avoid circular dependencies. +GraphEdge._BATCH_TYPE = GraphEdgeBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/components/graph_node.py b/rerun_py/rerun_sdk/rerun/components/graph_node.py new file mode 100644 index 000000000000..64514009428c --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/graph_node.py @@ -0,0 +1,32 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/components/graph_node.fbs". + +# You can extend this class by creating a "GraphNodeExt" class in "graph_node_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["GraphNode", "GraphNodeBatch"] + + +class GraphNode(datatypes.Utf8, ComponentMixin): + """**Component**: A string-based ID representing a node in a graph.""" + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of GraphNodeExt in graph_node_ext.py + + # Note: there are no fields here because GraphNode delegates to datatypes.Utf8 + pass + + +class GraphNodeBatch(datatypes.Utf8Batch, ComponentBatchMixin): + _COMPONENT_NAME: str = "rerun.components.GraphNode" + + +# This is patched in late to avoid circular dependencies. +GraphNode._BATCH_TYPE = GraphNodeBatch # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/components/graph_type.py b/rerun_py/rerun_sdk/rerun/components/graph_type.py new file mode 100644 index 000000000000..636d02315495 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/graph_type.py @@ -0,0 +1,68 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/components/graph_type.fbs". + +# You can extend this class by creating a "GraphTypeExt" class in "graph_type_ext.py". + +from __future__ import annotations + +from typing import Literal, Sequence, Union + +import pyarrow as pa + +from .._baseclasses import ( + BaseBatch, + ComponentBatchMixin, +) + +__all__ = ["GraphType", "GraphTypeArrayLike", "GraphTypeBatch", "GraphTypeLike"] + + +from enum import Enum + + +class GraphType(Enum): + """**Component**: Specifies if a graph has directed or undirected edges.""" + + Undirected = 1 + """The graph has undirected edges.""" + + Directed = 2 + """The graph has directed edges.""" + + @classmethod + def auto(cls, val: str | int | GraphType) -> GraphType: + """Best-effort converter, including a case-insensitive string matcher.""" + if isinstance(val, GraphType): + return val + if isinstance(val, int): + return cls(val) + try: + return cls[val] + except KeyError: + val_lower = val.lower() + for variant in cls: + if variant.name.lower() == val_lower: + return variant + raise ValueError(f"Cannot convert {val} to {cls.__name__}") + + def __str__(self) -> str: + """Returns the variant name.""" + return self.name + + +GraphTypeLike = Union[GraphType, Literal["Directed", "Undirected", "directed", "undirected"], int] +GraphTypeArrayLike = Union[GraphTypeLike, Sequence[GraphTypeLike]] + + +class GraphTypeBatch(BaseBatch[GraphTypeArrayLike], ComponentBatchMixin): + _ARROW_DATATYPE = pa.uint8() + _COMPONENT_NAME: str = "rerun.components.GraphType" + + @staticmethod + def _native_to_pa_array(data: GraphTypeArrayLike, data_type: pa.DataType) -> pa.Array: + if isinstance(data, (GraphType, int, str)): + data = [data] + + pa_data = [GraphType.auto(v).value if v is not None else None for v in data] # type: ignore[redundant-expr] + + return pa.array(pa_data, type=data_type) diff --git a/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes b/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes index 8f23c5dee36a..988472fc7b8a 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/datatypes/.gitattributes @@ -38,6 +38,7 @@ uint16.py linguist-generated=true uint32.py linguist-generated=true uint64.py linguist-generated=true utf8.py linguist-generated=true +utf8pair.py linguist-generated=true uuid.py linguist-generated=true uvec2d.py linguist-generated=true uvec3d.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/datatypes/__init__.py b/rerun_py/rerun_sdk/rerun/datatypes/__init__.py index a0142e682735..9a6dfb584c78 100644 --- a/rerun_py/rerun_sdk/rerun/datatypes/__init__.py +++ b/rerun_py/rerun_sdk/rerun/datatypes/__init__.py @@ -63,6 +63,7 @@ from .uint32 import UInt32, UInt32ArrayLike, UInt32Batch, UInt32Like from .uint64 import UInt64, UInt64ArrayLike, UInt64Batch, UInt64Like from .utf8 import Utf8, Utf8ArrayLike, Utf8Batch, Utf8Like +from .utf8pair import Utf8Pair, Utf8PairArrayLike, Utf8PairBatch, Utf8PairLike from .uuid import Uuid, UuidArrayLike, UuidBatch, UuidLike from .uvec2d import UVec2D, UVec2DArrayLike, UVec2DBatch, UVec2DLike from .uvec3d import UVec3D, UVec3DArrayLike, UVec3DBatch, UVec3DLike @@ -231,6 +232,10 @@ "Utf8ArrayLike", "Utf8Batch", "Utf8Like", + "Utf8Pair", + "Utf8PairArrayLike", + "Utf8PairBatch", + "Utf8PairLike", "Uuid", "UuidArrayLike", "UuidBatch", diff --git a/rerun_py/rerun_sdk/rerun/datatypes/utf8pair.py b/rerun_py/rerun_sdk/rerun/datatypes/utf8pair.py new file mode 100644 index 000000000000..ac1170c90195 --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/datatypes/utf8pair.py @@ -0,0 +1,85 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/datatypes/utf8_pair.fbs". + +# You can extend this class by creating a "Utf8PairExt" class in "utf8pair_ext.py". + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Sequence, Tuple, Union + +import numpy as np +import numpy.typing as npt +import pyarrow as pa +from attrs import define, field + +from .. import datatypes +from .._baseclasses import ( + BaseBatch, +) +from .utf8pair_ext import Utf8PairExt + +__all__ = ["Utf8Pair", "Utf8PairArrayLike", "Utf8PairBatch", "Utf8PairLike"] + + +def _utf8pair__first__special_field_converter_override(x: datatypes.Utf8Like) -> datatypes.Utf8: + if isinstance(x, datatypes.Utf8): + return x + else: + return datatypes.Utf8(x) + + +def _utf8pair__second__special_field_converter_override(x: datatypes.Utf8Like) -> datatypes.Utf8: + if isinstance(x, datatypes.Utf8): + return x + else: + return datatypes.Utf8(x) + + +@define(init=False) +class Utf8Pair(Utf8PairExt): + """**Datatype**: Stores a tuple of UTF-8 strings.""" + + def __init__(self: Any, first: datatypes.Utf8Like, second: datatypes.Utf8Like): + """ + Create a new instance of the Utf8Pair datatype. + + Parameters + ---------- + first: + The first string. + second: + The second string. + + """ + + # You can define your own __init__ function as a member of Utf8PairExt in utf8pair_ext.py + self.__attrs_init__(first=first, second=second) + + first: datatypes.Utf8 = field(converter=_utf8pair__first__special_field_converter_override) + # The first string. + # + # (Docstring intentionally commented out to hide this field from the docs) + + second: datatypes.Utf8 = field(converter=_utf8pair__second__special_field_converter_override) + # The second string. + # + # (Docstring intentionally commented out to hide this field from the docs) + + +if TYPE_CHECKING: + Utf8PairLike = Union[Utf8Pair, Tuple[datatypes.Utf8Like, datatypes.Utf8Like]] +else: + Utf8PairLike = Any + +Utf8PairArrayLike = Union[Utf8Pair, Sequence[Utf8PairLike], npt.NDArray[np.str_]] + + +class Utf8PairBatch(BaseBatch[Utf8PairArrayLike]): + _ARROW_DATATYPE = pa.struct([ + pa.field("first", pa.utf8(), nullable=False, metadata={}), + pa.field("second", pa.utf8(), nullable=False, metadata={}), + ]) + + @staticmethod + def _native_to_pa_array(data: Utf8PairArrayLike, data_type: pa.DataType) -> pa.Array: + return Utf8PairExt.native_to_pa_array_override(data, data_type) diff --git a/rerun_py/rerun_sdk/rerun/datatypes/utf8pair_ext.py b/rerun_py/rerun_sdk/rerun/datatypes/utf8pair_ext.py new file mode 100644 index 000000000000..5c99c84b2c9a --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/datatypes/utf8pair_ext.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pyarrow as pa + +if TYPE_CHECKING: + from . import ( + Utf8PairArrayLike, + ) + + +class Utf8PairExt: + """Extension for [Utf8Pair][rerun.datatypes.Utf8Pair].""" + + @staticmethod + def native_to_pa_array_override(data: Utf8PairArrayLike, data_type: pa.DataType) -> pa.Array: + from . import Utf8, Utf8Batch, Utf8Pair + + if isinstance(data, Utf8Pair): + first_string_batch = Utf8Batch(data.first) + second_string_batch = Utf8Batch(data.second) + elif isinstance(data, np.ndarray): + # We expect a 2-column array of string-compatible objects + if len(data.shape) != 2 or data.shape[1] != 2: + raise ValueError(f"Expected a 2-column numpy array, got an array with shape {data.shape}") + first_string_batch = Utf8Batch(data[:, 0]) + second_string_batch = Utf8Batch(data[:, 1]) + else: + # non-numpy Sequence[Utf8Pair | Tuple(Utf8Like, Utf8Like)] + first_strings: list[Utf8 | str] = [] + second_strings: list[Utf8 | str] = [] + for item in data: + if isinstance(item, Utf8Pair): + first_strings.append(item.first) + second_strings.append(item.second) + else: + first_strings.append(item[0]) + second_strings.append(item[1]) + first_string_batch = Utf8Batch(first_strings) + second_string_batch = Utf8Batch(second_strings) + + return pa.StructArray.from_arrays( + arrays=[first_string_batch.as_arrow_array(), second_string_batch.as_arrow_array()], + fields=[data_type.field("first"), data_type.field("second")], + ) diff --git a/rerun_py/tests/unit/test_utf8pair.py b/rerun_py/tests/unit/test_utf8pair.py new file mode 100644 index 000000000000..9f550b08b4f4 --- /dev/null +++ b/rerun_py/tests/unit/test_utf8pair.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import numpy as np +from rerun import datatypes + + +def test_utf8pair_batch_single() -> None: + single_pair_batches = [ + datatypes.Utf8PairBatch(datatypes.Utf8Pair("one", "two")), + datatypes.Utf8PairBatch([("one", "two")]), + datatypes.Utf8PairBatch([("one", datatypes.Utf8("two"))]), + datatypes.Utf8PairBatch([(datatypes.Utf8("one"), datatypes.Utf8("two"))]), + datatypes.Utf8PairBatch([(datatypes.Utf8("one"), "two")]), + datatypes.Utf8PairBatch(np.array([["one", "two"]])), + ] + + for batch in single_pair_batches[1:]: + assert single_pair_batches[0].as_arrow_array() == batch.as_arrow_array() + + +def test_utf8pair_batch_multiple() -> None: + multiple_pair_batches = [ + datatypes.Utf8PairBatch([datatypes.Utf8Pair("one", "two"), datatypes.Utf8Pair("three", "four")]), + datatypes.Utf8PairBatch([("one", "two"), ("three", "four")]), + datatypes.Utf8PairBatch(np.array([("one", "two"), ("three", "four")])), + ] + + for batch in multiple_pair_batches[1:]: + assert multiple_pair_batches[0].as_arrow_array() == batch.as_arrow_array() diff --git a/tests/python/release_checklist/check_all_components_ui.py b/tests/python/release_checklist/check_all_components_ui.py index ad488805f204..705f91b3b3b5 100644 --- a/tests/python/release_checklist/check_all_components_ui.py +++ b/tests/python/release_checklist/check_all_components_ui.py @@ -127,6 +127,15 @@ def alternatives(self) -> list[Any] | None: ((3, 3), (4, 4), (5, 5)), ] ), + "GraphEdgeBatch": TestCase( + batch=[ + rr.components.GraphEdge("a", "b"), + rr.components.GraphEdge("b", "c"), + rr.components.GraphEdge("c", "a"), + ] + ), + "GraphNodeBatch": TestCase(batch=["a", "b", "c"]), + "GraphTypeBatch": TestCase(rr.components.GraphType.Directed), "HalfSize2DBatch": TestCase(batch=[(5.0, 10.0), (50, 30), (23, 45)]), "HalfSize3DBatch": TestCase(batch=[(5.0, 10.0, 20.0), (50, 30, 40), (23, 45, 67)]), "ImageBufferBatch": TestCase( diff --git a/tests/python/release_checklist/check_graph_view.py b/tests/python/release_checklist/check_graph_view.py new file mode 100644 index 000000000000..5e6f60bdfc09 --- /dev/null +++ b/tests/python/release_checklist/check_graph_view.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import os +from argparse import Namespace +from uuid import uuid4 + +import rerun as rr +import rerun.blueprint as rrb + +README = """\ +# Graph view + +Please check the following: +* All graphs have a proper layout. +* `graph` has directed edges, while `graph2` has undirected edges. +* `graph` and `graph2` are shown in two different viewers. +* There is a third viewer, `Both`, that shows both `graph` and `graph2` in the same viewer. +""" + + +def log_readme() -> None: + rr.log("readme", rr.TextDocument(README, media_type=rr.MediaType.MARKDOWN), static=True) + + +def log_graphs() -> None: + DATA = [ + ("A", None), + ("B", None), + ("C", None), + (None, ("A", "B")), + (None, ("B", "C")), + (None, ("A", "C")), + ] + + nodes = [] + edges = [] + + for i, (new_node, new_edge) in enumerate(DATA): + if new_node is not None: + nodes.append(new_node) + if new_edge is not None: + edges.append(new_edge) + + rr.set_time_sequence("frame", i) + rr.log("graph", rr.GraphNodes(nodes, labels=nodes), rr.GraphEdges(edges, graph_type=rr.GraphType.Directed)) + rr.log("graph2", rr.GraphNodes(nodes, labels=nodes), rr.GraphEdges(edges, graph_type=rr.GraphType.Undirected)) + + rr.send_blueprint( + rrb.Blueprint( + rrb.Grid( + rrb.GraphView(origin="graph", name="Graph 1"), + rrb.GraphView(origin="graph2", name="Graph 2"), + rrb.GraphView(name="Both", contents=["/graph", "/graph2"]), + rrb.TextDocumentView(origin="readme", name="Instructions"), + ) + ) + ) + + +def run(args: Namespace) -> None: + rr.script_setup(args, f"{os.path.basename(__file__)}", recording_id=uuid4()) + + log_readme() + log_graphs() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Interactive release checklist") + rr.script_add_args(parser) + args = parser.parse_args() + run(args)