From 7b4b1f622350ec8d2a87d03cd88612f99e9f0347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 11 Nov 2024 13:57:42 +0100 Subject: [PATCH 01/12] WIP: experiment with auto-layout --- Cargo.lock | 7 + Cargo.toml | 1 + crates/viewer/re_space_view_graph/Cargo.toml | 2 + .../re_space_view_graph/src/properties.rs | 2 +- .../re_space_view_graph/src/ui/state.rs | 5 +- crates/viewer/re_space_view_graph/src/view.rs | 101 ++++++++++--- crates/viewer/re_viewer/src/reflection/mod.rs | 16 +- docs/content/reference/types/components.md | 141 +++++++++--------- .../reference/types/components/graph_edge.md | 12 +- .../reference/types/components/graph_node.md | 9 +- .../reference/types/components/graph_type.md | 12 +- .../reference/types/datatypes/utf8pair.md | 19 ++- 12 files changed, 221 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e276d40e34dc..3f3bc70b2642 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2418,6 +2418,11 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fjadra" +version = "0.1.0" +source = "git+https://github.com/grtlr/fjadra.git#40027d0c4d83096142509cb54b349313643de5e4" + [[package]] name = "flatbuffers" version = "23.5.26" @@ -6159,9 +6164,11 @@ version = "0.20.0-alpha.4+dev" dependencies = [ "ahash", "egui", + "fjadra", "nohash-hasher", "re_chunk", "re_format", + "re_log", "re_log_types", "re_query", "re_renderer", diff --git a/Cargo.toml b/Cargo.toml index 8f6b2699e672..1430f51cd7f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -181,6 +181,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 = { version = "0.1", default-features = false, git = "https://github.com/grtlr/fjadra.git" } flatbuffers = "23.0" futures-channel = "0.3" futures-util = { version = "0.3", default-features = false } diff --git a/crates/viewer/re_space_view_graph/Cargo.toml b/crates/viewer/re_space_view_graph/Cargo.toml index 5f7f38de5684..e2c349307131 100644 --- a/crates/viewer/re_space_view_graph/Cargo.toml +++ b/crates/viewer/re_space_view_graph/Cargo.toml @@ -21,6 +21,7 @@ all-features = true [dependencies] re_chunk.workspace = true re_format.workspace = true +re_log.workspace = true re_log_types.workspace = true re_query.workspace = true re_renderer.workspace = true @@ -33,4 +34,5 @@ re_viewport_blueprint.workspace = true ahash.workspace = true egui.workspace = true +fjadra.workspace = true nohash-hasher.workspace = true diff --git a/crates/viewer/re_space_view_graph/src/properties.rs b/crates/viewer/re_space_view_graph/src/properties.rs index 0c0ddebbd134..02a673e9feb0 100644 --- a/crates/viewer/re_space_view_graph/src/properties.rs +++ b/crates/viewer/re_space_view_graph/src/properties.rs @@ -20,7 +20,7 @@ impl TypedComponentFallbackProvider for GraphSpaceView { return VisualBounds2D::default(); }; - let default_scene_rect = bounding_rect_from_iter(layout.values()); + let default_scene_rect = bounding_rect_from_iter(layout.1.values()); if valid_bound(&default_scene_rect) { default_scene_rect.into() diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 9bae591806b2..39fd13f974df 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use re_chunk::{TimeInt, Timeline}; use re_format::format_f32; use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; @@ -16,7 +17,7 @@ use super::bounding_rect_from_iter; pub struct GraphSpaceViewState { /// Positions of the nodes in world space. If the layout is `None`, the /// nodes were never layed out. - pub layout: Option>, + pub layout: Option<((Timeline, TimeInt), HashMap)>, pub show_debug: bool, @@ -32,7 +33,7 @@ impl GraphSpaceViewState { .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 } = bounding_rect_from_iter(layout.values()); + let egui::Rect { min, max } = bounding_rect_from_iter(layout.1.values()); ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); }); diff --git a/crates/viewer/re_space_view_graph/src/view.rs b/crates/viewer/re_space_view_graph/src/view.rs index 817f3151bba1..9b9e038650b1 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -1,7 +1,10 @@ use std::collections::{BTreeSet, HashSet}; +use ahash::HashMap; use egui::{self, Vec2}; +use fjadra::{Center, Link, ManyBody, PositionX, PositionY, SimulationBuilder}; +use re_log::external::log; use re_log_types::EntityPath; use re_space_view::view_property_ui; use re_types::{ @@ -65,7 +68,7 @@ impl SpaceViewClass for GraphSpaceView { let (width, height) = state.world_bounds.map_or_else( || { - let bbox = bounding_rect_from_iter(layout.values()); + let bbox = bounding_rect_from_iter(layout.1.values()); ( (bbox.max.x - bbox.min.x).abs(), (bbox.max.y - bbox.min.y).abs(), @@ -159,7 +162,71 @@ impl SpaceViewClass for GraphSpaceView { // For now, we reset the layout at every frame. Eventually, we want // to keep information between frames so that the nodes don't jump around. - let layout = state.layout.insert(Default::default()); + // let (layout_time, layout) = state + // .layout + // .insert(((query.timeline, query.latest_at), Default::default())); + + let layout = match state.layout { + Some(ref mut layout) + if (layout.0 .0, layout.0 .1) == (query.timeline, query.latest_at) => + { + &mut layout.1 + } + _ => { + log::debug!("recomputing graph layout"); + + let layout = state + .layout + .insert(((query.timeline, query.latest_at), Default::default())); + + let mut node_index: HashMap = HashMap::default(); + let mut all_nodes = node_data + .values() + .flat_map(|data| data.nodes.iter().map(|n| n.index)) + .enumerate() + .map(|(o, n)| { + node_index.insert(n, o); + n + }) + .collect::>(); + + let mut all_edges: Vec<(usize, usize)> = Vec::new(); + for edge in edge_data.values().flat_map(|data| data.edges.iter()) { + let source = *node_index.entry(edge.source_index).or_insert_with(|| { + all_nodes.push(edge.source_index); + all_nodes.len() - 1 + }); + + let target = *node_index.entry(edge.target_index).or_insert_with(|| { + all_nodes.push(edge.target_index); + all_nodes.len() - 1 + }); + all_edges.push((source, target)); + } + + let mut simulation = SimulationBuilder::default() + .build(all_nodes.iter().map(|_| Option::<[f64; 2]>::None)) + .add_force( + "link", + Link::new(all_edges.into_iter()), + ) + .add_force("charge", ManyBody::new()) + .add_force("x", PositionX::new()) + .add_force("y", PositionY::new()); + + let positions = simulation.iter().last().expect("simulation should run"); + for (node, i) in node_index { + layout.1.entry(node).or_insert_with(|| { + let pos = positions[i]; + let pos = egui::Pos2::new(pos[0] as f32, pos[1] as f32); + let size = egui::Vec2::ZERO; + egui::Rect::from_min_size(pos, size) + }); + } + + &mut layout.1 + } + }; state.world_bounds = Some(bounds); let bounds_rect: egui::Rect = bounds.into(); @@ -241,21 +308,21 @@ impl SpaceViewClass for GraphSpaceView { } } - if 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(), - ); - - // TODO(grtlr): Should take padding from `draw_entity` into account. - // It's very likely that this part of the code is going to change once we introduce auto-layout. - let between_entities = 80.0; - entity_offset.x += entity_rect.width() + between_entities; - } + // if 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(), + // ); + + // // TODO(grtlr): Should take padding from `draw_entity` into account. + // // It's very likely that this part of the code is going to change once we introduce auto-layout. + // let between_entities = 80.0; + // // entity_offset.x += entity_rect.width() + between_entities; + // } } }); diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index 228cbfa1a81b..854fc4953a2b 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -438,6 +438,14 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "A geospatial line string expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees).", + custom_placeholder: Some(GeoLineString::default().to_arrow()?), + datatype: GeoLineString::arrow_datatype(), + }, + ), ( ::name(), ComponentReflection { @@ -462,14 +470,6 @@ fn generate_component_reflection() -> Result::name(), - ComponentReflection { - docstring_md: "A geospatial line string expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees).", - custom_placeholder: Some(GeoLineString::default().to_arrow()?), - datatype: GeoLineString::arrow_datatype(), - }, - ), ( ::name(), ComponentReflection { diff --git a/docs/content/reference/types/components.md b/docs/content/reference/types/components.md index f7ff9c20b2e1..e95094cca0c5 100644 --- a/docs/content/reference/types/components.md +++ b/docs/content/reference/types/components.md @@ -2,7 +2,6 @@ title: "Components" order: 2 --- - Components are the fundamental unit of logging in Rerun. This page lists all built-in components. @@ -13,72 +12,74 @@ If you log the same component several times on an entity, the last value (or arr For more information on the relationship between **archetypes** and **components**, check out the concept page on [Entities and Components](../../concepts/entity-component.md). -- [`AggregationPolicy`](components/aggregation_policy.md): Policy for aggregation of multiple scalar plot values. -- [`AlbedoFactor`](components/albedo_factor.md): A color multiplier, usually applied to a whole entity, e.g. a mesh. -- [`AnnotationContext`](components/annotation_context.md): The annotation context provides additional information on how to display entities. -- [`AxisLength`](components/axis_length.md): The length of an axis in local units of the space. -- [`Blob`](components/blob.md): A binary blob of data. -- [`ClassId`](components/class_id.md): A 16-bit ID representing a type of semantic class. -- [`ClearIsRecursive`](components/clear_is_recursive.md): Configures how a clear operation should behave - recursive or not. -- [`Color`](components/color.md): An RGBA color with unmultiplied/separate alpha, in sRGB gamma space with linear alpha. -- [`Colormap`](components/colormap.md): Colormap for mapping scalar values within a given range to a color. -- [`DepthMeter`](components/depth_meter.md): The world->depth map scaling factor. -- [`DisconnectedSpace`](components/disconnected_space.md): Spatially disconnect this entity from its parent. -- [`DrawOrder`](components/draw_order.md): Draw order of 2D elements. Higher values are drawn on top of lower values. -- [`EntityPath`](components/entity_path.md): A path to an entity, usually to reference some data that is part of the target entity. -- [`FillMode`](components/fill_mode.md): How a geometric shape is drawn and colored. -- [`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. -- [`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. -- [`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). -- [`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. -- [`ImageFormat`](components/image_format.md): The metadata describing the contents of a [`components.ImageBuffer`](https://rerun.io/docs/reference/types/components/image_buffer). -- [`ImagePlaneDistance`](components/image_plane_distance.md): The distance from the camera origin to the image plane when the projection is shown in a 3D viewer. -- [`KeypointId`](components/keypoint_id.md): A 16-bit ID representing a type of semantic keypoint within a class. -- [`LatLon`](components/lat_lon.md): A geospatial position expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees). -- [`Length`](components/length.md): Length, or one-dimensional size. -- [`LineStrip2D`](components/line_strip2d.md): A line strip in 2D space. -- [`LineStrip3D`](components/line_strip3d.md): A line strip in 3D space. -- [`MagnificationFilter`](components/magnification_filter.md): Filter used when magnifying an image/texture such that a single pixel/texel is displayed as multiple pixels on screen. -- [`MarkerShape`](components/marker_shape.md): The visual appearance of a point in e.g. a 2D plot. -- [`MarkerSize`](components/marker_size.md): Radius of a marker of a point in e.g. a 2D plot, measured in UI points. -- [`MediaType`](components/media_type.md): A standardized media type (RFC2046, formerly known as MIME types), encoded as a string. -- [`Name`](components/name.md): A display name, typically for an entity or a item like a plot series. -- [`Opacity`](components/opacity.md): Degree of transparency ranging from 0.0 (fully transparent) to 1.0 (fully opaque). -- [`PinholeProjection`](components/pinhole_projection.md): Camera projection, from image coordinates to view coordinates. -- [`PoseRotationAxisAngle`](components/pose_rotation_axis_angle.md): 3D rotation represented by a rotation around a given axis that doesn't propagate in the transform hierarchy. -- [`PoseRotationQuat`](components/pose_rotation_quat.md): A 3D rotation expressed as a quaternion that doesn't propagate in the transform hierarchy. -- [`PoseScale3D`](components/pose_scale3d.md): A 3D scale factor that doesn't propagate in the transform hierarchy. -- [`PoseTransformMat3x3`](components/pose_transform_mat3x3.md): A 3x3 transformation matrix Matrix that doesn't propagate in the transform hierarchy. -- [`PoseTranslation3D`](components/pose_translation3d.md): A translation vector in 3D space that doesn't propagate in the transform hierarchy. -- [`Position2D`](components/position2d.md): A position in 2D space. -- [`Position3D`](components/position3d.md): A position in 3D space. -- [`Radius`](components/radius.md): The radius of something, e.g. a point. -- [`Range1D`](components/range1d.md): A 1D range, specifying a lower and upper bound. -- [`Resolution`](components/resolution.md): Pixel resolution width & height, e.g. of a camera sensor. -- [`RotationAxisAngle`](components/rotation_axis_angle.md): 3D rotation represented by a rotation around a given axis. -- [`RotationQuat`](components/rotation_quat.md): A 3D rotation expressed as a quaternion. -- [`Scalar`](components/scalar.md): A scalar value, encoded as a 64-bit floating point. -- [`Scale3D`](components/scale3d.md): A 3D scale factor. -- [`ShowLabels`](components/show_labels.md): Whether the entity's [`components.Text`](https://rerun.io/docs/reference/types/components/text) label is shown. -- [`StrokeWidth`](components/stroke_width.md): The width of a stroke specified in UI points. -- [`TensorData`](components/tensor_data.md): An N-dimensional array of numbers. -- [`TensorDimensionIndexSelection`](components/tensor_dimension_index_selection.md): Specifies a concrete index on a tensor dimension. -- [`TensorHeightDimension`](components/tensor_height_dimension.md): Specifies which dimension to use for height. -- [`TensorWidthDimension`](components/tensor_width_dimension.md): Specifies which dimension to use for width. -- [`Texcoord2D`](components/texcoord2d.md): A 2D texture UV coordinate. -- [`Text`](components/text.md): A string of text, e.g. for labels and text documents. -- [`TextLogLevel`](components/text_log_level.md): The severity level of a text log message. -- [`TransformMat3x3`](components/transform_mat3x3.md): A 3x3 transformation matrix Matrix. -- [`TransformRelation`](components/transform_relation.md): Specifies relation a spatial transform describes. -- [`Translation3D`](components/translation3d.md): A translation vector in 3D space. -- [`TriangleIndices`](components/triangle_indices.md): The three indices of a triangle in a triangle mesh. -- [`ValueRange`](components/value_range.md): Range of expected or valid values, specifying a lower and upper bound. -- [`Vector2D`](components/vector2d.md): A vector in 2D space. -- [`Vector3D`](components/vector3d.md): A vector in 3D space. -- [`VideoTimestamp`](components/video_timestamp.md): Timestamp inside a [`archetypes.AssetVideo`](https://rerun.io/docs/reference/types/archetypes/asset_video). -- [`ViewCoordinates`](components/view_coordinates.md): How we interpret the coordinate system of an entity/space. + +* [`AggregationPolicy`](components/aggregation_policy.md): Policy for aggregation of multiple scalar plot values. +* [`AlbedoFactor`](components/albedo_factor.md): A color multiplier, usually applied to a whole entity, e.g. a mesh. +* [`AnnotationContext`](components/annotation_context.md): The annotation context provides additional information on how to display entities. +* [`AxisLength`](components/axis_length.md): The length of an axis in local units of the space. +* [`Blob`](components/blob.md): A binary blob of data. +* [`ClassId`](components/class_id.md): A 16-bit ID representing a type of semantic class. +* [`ClearIsRecursive`](components/clear_is_recursive.md): Configures how a clear operation should behave - recursive or not. +* [`Color`](components/color.md): An RGBA color with unmultiplied/separate alpha, in sRGB gamma space with linear alpha. +* [`Colormap`](components/colormap.md): Colormap for mapping scalar values within a given range to a color. +* [`DepthMeter`](components/depth_meter.md): The world->depth map scaling factor. +* [`DisconnectedSpace`](components/disconnected_space.md): Spatially disconnect this entity from its parent. +* [`DrawOrder`](components/draw_order.md): Draw order of 2D elements. Higher values are drawn on top of lower values. +* [`EntityPath`](components/entity_path.md): A path to an entity, usually to reference some data that is part of the target entity. +* [`FillMode`](components/fill_mode.md): How a geometric shape is drawn and colored. +* [`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. +* [`ImageFormat`](components/image_format.md): The metadata describing the contents of a [`components.ImageBuffer`](https://rerun.io/docs/reference/types/components/image_buffer). +* [`ImagePlaneDistance`](components/image_plane_distance.md): The distance from the camera origin to the image plane when the projection is shown in a 3D viewer. +* [`KeypointId`](components/keypoint_id.md): A 16-bit ID representing a type of semantic keypoint within a class. +* [`LatLon`](components/lat_lon.md): A geospatial position expressed in [EPSG:4326](https://epsg.io/4326) latitude and longitude (North/East-positive degrees). +* [`Length`](components/length.md): Length, or one-dimensional size. +* [`LineStrip2D`](components/line_strip2d.md): A line strip in 2D space. +* [`LineStrip3D`](components/line_strip3d.md): A line strip in 3D space. +* [`MagnificationFilter`](components/magnification_filter.md): Filter used when magnifying an image/texture such that a single pixel/texel is displayed as multiple pixels on screen. +* [`MarkerShape`](components/marker_shape.md): The visual appearance of a point in e.g. a 2D plot. +* [`MarkerSize`](components/marker_size.md): Radius of a marker of a point in e.g. a 2D plot, measured in UI points. +* [`MediaType`](components/media_type.md): A standardized media type (RFC2046, formerly known as MIME types), encoded as a string. +* [`Name`](components/name.md): A display name, typically for an entity or a item like a plot series. +* [`Opacity`](components/opacity.md): Degree of transparency ranging from 0.0 (fully transparent) to 1.0 (fully opaque). +* [`PinholeProjection`](components/pinhole_projection.md): Camera projection, from image coordinates to view coordinates. +* [`PoseRotationAxisAngle`](components/pose_rotation_axis_angle.md): 3D rotation represented by a rotation around a given axis that doesn't propagate in the transform hierarchy. +* [`PoseRotationQuat`](components/pose_rotation_quat.md): A 3D rotation expressed as a quaternion that doesn't propagate in the transform hierarchy. +* [`PoseScale3D`](components/pose_scale3d.md): A 3D scale factor that doesn't propagate in the transform hierarchy. +* [`PoseTransformMat3x3`](components/pose_transform_mat3x3.md): A 3x3 transformation matrix Matrix that doesn't propagate in the transform hierarchy. +* [`PoseTranslation3D`](components/pose_translation3d.md): A translation vector in 3D space that doesn't propagate in the transform hierarchy. +* [`Position2D`](components/position2d.md): A position in 2D space. +* [`Position3D`](components/position3d.md): A position in 3D space. +* [`Radius`](components/radius.md): The radius of something, e.g. a point. +* [`Range1D`](components/range1d.md): A 1D range, specifying a lower and upper bound. +* [`Resolution`](components/resolution.md): Pixel resolution width & height, e.g. of a camera sensor. +* [`RotationAxisAngle`](components/rotation_axis_angle.md): 3D rotation represented by a rotation around a given axis. +* [`RotationQuat`](components/rotation_quat.md): A 3D rotation expressed as a quaternion. +* [`Scalar`](components/scalar.md): A scalar value, encoded as a 64-bit floating point. +* [`Scale3D`](components/scale3d.md): A 3D scale factor. +* [`ShowLabels`](components/show_labels.md): Whether the entity's [`components.Text`](https://rerun.io/docs/reference/types/components/text) label is shown. +* [`StrokeWidth`](components/stroke_width.md): The width of a stroke specified in UI points. +* [`TensorData`](components/tensor_data.md): An N-dimensional array of numbers. +* [`TensorDimensionIndexSelection`](components/tensor_dimension_index_selection.md): Specifies a concrete index on a tensor dimension. +* [`TensorHeightDimension`](components/tensor_height_dimension.md): Specifies which dimension to use for height. +* [`TensorWidthDimension`](components/tensor_width_dimension.md): Specifies which dimension to use for width. +* [`Texcoord2D`](components/texcoord2d.md): A 2D texture UV coordinate. +* [`Text`](components/text.md): A string of text, e.g. for labels and text documents. +* [`TextLogLevel`](components/text_log_level.md): The severity level of a text log message. +* [`TransformMat3x3`](components/transform_mat3x3.md): A 3x3 transformation matrix Matrix. +* [`TransformRelation`](components/transform_relation.md): Specifies relation a spatial transform describes. +* [`Translation3D`](components/translation3d.md): A translation vector in 3D space. +* [`TriangleIndices`](components/triangle_indices.md): The three indices of a triangle in a triangle mesh. +* [`ValueRange`](components/value_range.md): Range of expected or valid values, specifying a lower and upper bound. +* [`Vector2D`](components/vector2d.md): A vector in 2D space. +* [`Vector3D`](components/vector3d.md): A vector in 3D space. +* [`VideoTimestamp`](components/video_timestamp.md): Timestamp inside a [`archetypes.AssetVideo`](https://rerun.io/docs/reference/types/archetypes/asset_video). +* [`ViewCoordinates`](components/view_coordinates.md): How we interpret the coordinate system of an entity/space. + diff --git a/docs/content/reference/types/components/graph_edge.md b/docs/content/reference/types/components/graph_edge.md index 247390628179..572c0f45abbe 100644 --- a/docs/content/reference/types/components/graph_edge.md +++ b/docs/content/reference/types/components/graph_edge.md @@ -5,9 +5,17 @@ title: "GraphEdge" An edge in a graph connecting two nodes. -## Fields +## Rerun datatype +[`Utf8Pair`](../datatypes/utf8pair.md) -* edge: [`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) diff --git a/docs/content/reference/types/components/graph_node.md b/docs/content/reference/types/components/graph_node.md index 6933b7fbc5c4..63339a42164e 100644 --- a/docs/content/reference/types/components/graph_node.md +++ b/docs/content/reference/types/components/graph_node.md @@ -5,9 +5,14 @@ title: "GraphNode" A string-based ID representing a node in a graph. -## Fields +## Rerun datatype +[`Utf8`](../datatypes/utf8.md) -* id: [`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) diff --git a/docs/content/reference/types/components/graph_type.md b/docs/content/reference/types/components/graph_type.md index 77f0207a6f9d..30a9869f4431 100644 --- a/docs/content/reference/types/components/graph_type.md +++ b/docs/content/reference/types/components/graph_type.md @@ -6,9 +6,17 @@ title: "GraphType" Specifies if a graph has directed or undirected edges. ## Variants +#### `Undirected` = 1 +The graph has undirected edges. -* Undirected -* Directed +#### `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) diff --git a/docs/content/reference/types/datatypes/utf8pair.md b/docs/content/reference/types/datatypes/utf8pair.md index 094e1601485a..a1c4c272f99b 100644 --- a/docs/content/reference/types/datatypes/utf8pair.md +++ b/docs/content/reference/types/datatypes/utf8pair.md @@ -6,9 +6,24 @@ title: "Utf8Pair" Stores a tuple of UTF-8 strings. ## Fields +#### `first` +Type: [`Utf8`](../datatypes/utf8.md) -* first: [`Utf8`](../datatypes/utf8.md) -* second: [`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) From 4ab3024e3e639bbca99125dbafd2752705df74b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 11 Nov 2024 17:06:59 +0100 Subject: [PATCH 02/12] WIP: basic auto-layout working --- .../viewer/re_space_view_graph/src/layout.rs | 95 ++++++++++++ crates/viewer/re_space_view_graph/src/lib.rs | 1 + .../re_space_view_graph/src/properties.rs | 7 +- .../re_space_view_graph/src/ui/state.rs | 11 +- crates/viewer/re_space_view_graph/src/view.rs | 145 +++++------------- .../src/visualizers/mod.rs | 43 +++++- 6 files changed, 178 insertions(+), 124 deletions(-) create mode 100644 crates/viewer/re_space_view_graph/src/layout.rs diff --git a/crates/viewer/re_space_view_graph/src/layout.rs b/crates/viewer/re_space_view_graph/src/layout.rs new file mode 100644 index 000000000000..8ced23adfffd --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/layout.rs @@ -0,0 +1,95 @@ +use egui::Rect; +use fjadra::{Center, Link, ManyBody, PositionX, PositionY, SimulationBuilder}; +use re_chunk::{EntityPath, TimeInt, Timeline}; + +use crate::{ + graph::NodeIndex, + ui::bounding_rect_from_iter, + visualizers::{all_edges, all_nodes, EdgeData, NodeData}, +}; + +/// Used to determine if a layout is up-to-date or outdated. +#[derive(Debug, PartialEq, Eq)] +pub struct Timestamp { + timeline: Timeline, + time: TimeInt, +} + +pub struct Layout { + valid_at: Timestamp, + extents: ahash::HashMap, +} + +impl std::fmt::Debug for Layout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Layout") + .field("valid_at", &self.valid_at) + .finish() + } +} + +impl Layout { + pub fn needs_update(&self, timeline: Timeline, time: TimeInt) -> bool { + self.valid_at.timeline != timeline || self.valid_at.time != time + } + + pub fn bounding_rect(&self) -> Rect { + bounding_rect_from_iter(self.extents.values()) + } + + pub fn get(&self, node: &NodeIndex) -> Option<&Rect> { + self.extents.get(node) + } + + pub fn update(&mut self, node: &NodeIndex, rect: Rect) { + *self + .extents + .get_mut(node) + .expect("node should exist in layout") = rect; + } +} + +pub struct LayoutProvider; + +impl LayoutProvider { + pub fn compute<'a>( + timeline: Timeline, + time: TimeInt, + nodes: impl IntoIterator, + edges: impl IntoIterator + Clone, + ) -> Layout { + // Will hold the positions of the nodes, stored as bounding rectangles. + let mut extents = ahash::HashMap::default(); + + let nodes = all_nodes(nodes, edges.clone()) + .map(|n| n.1) + .collect::>(); + let node_index: ahash::HashMap = + nodes.iter().enumerate().map(|(i, n)| (*n, i)).collect(); + + let edges = all_edges(edges) + .map(|(_, (source, target))| (node_index[&source], node_index[&target])); + + let mut simulation = SimulationBuilder::default() + .build(nodes.iter().map(|_| Option::<[f64; 2]>::None)) + .add_force("link", Link::new(edges)) + .add_force("charge", ManyBody::new().strength(-300.0)) + .add_force("x", PositionX::new()) + .add_force("y", PositionY::new()); + + let positions = simulation.iter().last().expect("simulation should run"); + for (node, i) in node_index { + extents.entry(node).or_insert_with(|| { + let pos = positions[i]; + let pos = egui::Pos2::new(pos[0] as f32, pos[1] as f32); + let size = egui::Vec2::ZERO; + egui::Rect::from_min_size(pos, size) + }); + } + + Layout { + valid_at: Timestamp { timeline, time }, + extents, + } + } +} diff --git a/crates/viewer/re_space_view_graph/src/lib.rs b/crates/viewer/re_space_view_graph/src/lib.rs index 9edbaacfd2cf..5e05c7cd698c 100644 --- a/crates/viewer/re_space_view_graph/src/lib.rs +++ b/crates/viewer/re_space_view_graph/src/lib.rs @@ -3,6 +3,7 @@ //! A Space View that shows a graph (node-link diagram). mod graph; +mod layout; mod properties; mod types; mod ui; diff --git a/crates/viewer/re_space_view_graph/src/properties.rs b/crates/viewer/re_space_view_graph/src/properties.rs index 02a673e9feb0..bbc13844e535 100644 --- a/crates/viewer/re_space_view_graph/src/properties.rs +++ b/crates/viewer/re_space_view_graph/src/properties.rs @@ -1,10 +1,7 @@ use re_types::blueprint::components::VisualBounds2D; use re_viewer_context::{SpaceViewStateExt as _, TypedComponentFallbackProvider}; -use crate::{ - ui::{bounding_rect_from_iter, GraphSpaceViewState}, - GraphSpaceView, -}; +use crate::{ui::GraphSpaceViewState, GraphSpaceView}; fn valid_bound(rect: &egui::Rect) -> bool { rect.is_finite() && rect.is_positive() @@ -20,7 +17,7 @@ impl TypedComponentFallbackProvider for GraphSpaceView { return VisualBounds2D::default(); }; - let default_scene_rect = bounding_rect_from_iter(layout.1.values()); + let default_scene_rect = layout.bounding_rect(); if valid_bound(&default_scene_rect) { default_scene_rect.into() diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 39fd13f974df..c1f0ee0e3d7d 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -1,12 +1,9 @@ -use std::collections::HashMap; - -use re_chunk::{TimeInt, Timeline}; use re_format::format_f32; use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; use re_viewer_context::SpaceViewState; -use crate::graph::NodeIndex; +use crate::layout::Layout; use super::bounding_rect_from_iter; @@ -15,9 +12,7 @@ use super::bounding_rect_from_iter; /// This state is preserved between frames, but not across Viewer sessions. #[derive(Default)] pub struct GraphSpaceViewState { - /// Positions of the nodes in world space. If the layout is `None`, the - /// nodes were never layed out. - pub layout: Option<((Timeline, TimeInt), HashMap)>, + pub layout: Option, pub show_debug: bool, @@ -33,7 +28,7 @@ impl GraphSpaceViewState { .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 } = bounding_rect_from_iter(layout.1.values()); + let egui::Rect { min, max } = layout.bounding_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),)); }); diff --git a/crates/viewer/re_space_view_graph/src/view.rs b/crates/viewer/re_space_view_graph/src/view.rs index 9b9e038650b1..ad5fdbdea91c 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -3,7 +3,6 @@ use std::collections::{BTreeSet, HashSet}; use ahash::HashMap; use egui::{self, Vec2}; -use fjadra::{Center, Link, ManyBody, PositionX, PositionY, SimulationBuilder}; use re_log::external::log; use re_log_types::EntityPath; use re_space_view::view_property_ui; @@ -23,6 +22,7 @@ use re_viewport_blueprint::ViewProperty; use crate::{ graph::NodeIndex, + layout::LayoutProvider, ui::{bounding_rect_from_iter, canvas::CanvasBuilder, GraphSpaceViewState}, visualizers::{EdgesVisualizer, NodeVisualizer}, }; @@ -68,11 +68,8 @@ impl SpaceViewClass for GraphSpaceView { let (width, height) = state.world_bounds.map_or_else( || { - let bbox = bounding_rect_from_iter(layout.1.values()); - ( - (bbox.max.x - bbox.min.x).abs(), - (bbox.max.y - bbox.min.y).abs(), - ) + let bbox = layout.bounding_rect(); + (bbox.width().abs(), bbox.height().abs()) }, |bounds| { ( @@ -160,71 +157,21 @@ impl SpaceViewClass for GraphSpaceView { let layout_was_empty = state.layout.is_none(); - // For now, we reset the layout at every frame. Eventually, we want - // to keep information between frames so that the nodes don't jump around. - // let (layout_time, layout) = state - // .layout - // .insert(((query.timeline, query.latest_at), Default::default())); - - let layout = match state.layout { - Some(ref mut layout) - if (layout.0 .0, layout.0 .1) == (query.timeline, query.latest_at) => - { - &mut layout.1 + let layout = match &mut state.layout { + Some(layout) if !layout.needs_update(query.timeline, query.latest_at) => { + layout // Layout is up to date, reuse it. } _ => { - log::debug!("recomputing graph layout"); - - let layout = state - .layout - .insert(((query.timeline, query.latest_at), Default::default())); - - let mut node_index: HashMap = HashMap::default(); - let mut all_nodes = node_data - .values() - .flat_map(|data| data.nodes.iter().map(|n| n.index)) - .enumerate() - .map(|(o, n)| { - node_index.insert(n, o); - n - }) - .collect::>(); - - let mut all_edges: Vec<(usize, usize)> = Vec::new(); - for edge in edge_data.values().flat_map(|data| data.edges.iter()) { - let source = *node_index.entry(edge.source_index).or_insert_with(|| { - all_nodes.push(edge.source_index); - all_nodes.len() - 1 - }); - - let target = *node_index.entry(edge.target_index).or_insert_with(|| { - all_nodes.push(edge.target_index); - all_nodes.len() - 1 - }); - all_edges.push((source, target)); - } + log::debug!("Recomputing graph layout"); - let mut simulation = SimulationBuilder::default() - .build(all_nodes.iter().map(|_| Option::<[f64; 2]>::None)) - .add_force( - "link", - Link::new(all_edges.into_iter()), - ) - .add_force("charge", ManyBody::new()) - .add_force("x", PositionX::new()) - .add_force("y", PositionY::new()); - - let positions = simulation.iter().last().expect("simulation should run"); - for (node, i) in node_index { - layout.1.entry(node).or_insert_with(|| { - let pos = positions[i]; - let pos = egui::Pos2::new(pos[0] as f32, pos[1] as f32); - let size = egui::Vec2::ZERO; - egui::Rect::from_min_size(pos, size) - }); - } + let layout = LayoutProvider::compute( + query.timeline, + query.latest_at, + node_data.iter(), + edge_data.iter(), + ); - &mut layout.1 + state.layout.insert(layout) } }; @@ -238,10 +185,6 @@ impl SpaceViewClass for GraphSpaceView { viewer.show_debug(); } - // We keep track of the nodes in the data to clean up the layout. - // TODO(grtlr): once we settle on a design, it might make sense to create a - // `Layout` struct that keeps track of the layout and the nodes that - // get added and removed and cleans up automatically (guard pattern). let mut seen: HashSet = HashSet::new(); let (new_world_bounds, response) = viewer.canvas(ui, |mut scene| { @@ -251,17 +194,16 @@ impl SpaceViewClass for GraphSpaceView { for entity in entities { // We keep track of the size of the current entity. + let mut entity_rect = egui::Rect::NOTHING; if let Some(data) = node_data.get(entity) { for node in &data.nodes { seen.insert(node.index); - let current = layout.entry(node.index).or_insert(scene.initial_rect(node)); - - let response = scene.explicit_node(current.min + entity_offset, node); - - // TODO(grtlr): ⚠️ This is hacky: - // We need to undo the `entity_offset` otherwise the offset will increase each frame. - *current = response.rect.translate(-entity_offset); + let pos = layout + .get(&node.index) + .expect("explicit node should be in layout"); + let response = scene.explicit_node(pos.min, node); + layout.update(&node.index, response.rect); entity_rect = entity_rect.union(response.rect); } } @@ -275,29 +217,19 @@ impl SpaceViewClass for GraphSpaceView { .filter(|n| !seen.contains(&NodeIndex::from_entity_node(entity, n))) .collect::>(); - // TODO(grtlr): The following logic is quite hacky, because we have to place the implicit nodes somewhere. - // A lot of this logic will probably go away once we ship auto-layouts. - let mut current_implicit_offset = - Vec2::new(entity_rect.min.x, entity_rect.height() + 40.0); for node in implicit_nodes { let ix = NodeIndex::from_entity_node(entity, node); seen.insert(ix); - let current = layout.entry(ix).or_insert( - egui::Rect::ZERO - .translate(entity_offset) - .translate(current_implicit_offset), - ); + let current = layout.get(&ix).expect("implicit node should be in layout"); let response = scene.implicit_node(current.min, node); - *current = response.rect.translate(-entity_offset); - // entity_rect = entity_rect.union(response.rect); - current_implicit_offset.x += 10.0; + layout.update(&ix, response.rect); + entity_rect = entity_rect.union(response.rect); } for edge in &data.edges { - if let (Some(source_pos), Some(target_pos)) = ( - layout.get(&edge.source_index), - layout.get(&edge.target_index), - ) { + if let (Some(source_pos), Some(target_pos)) = + (layout.get(&edge.source_index), layout.get(&edge.target_index)) + { scene.edge( source_pos.translate(entity_offset), target_pos.translate(entity_offset), @@ -308,21 +240,16 @@ impl SpaceViewClass for GraphSpaceView { } } - // if 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(), - // ); - - // // TODO(grtlr): Should take padding from `draw_entity` into account. - // // It's very likely that this part of the code is going to change once we introduce auto-layout. - // let between_entities = 80.0; - // // entity_offset.x += entity_rect.width() + between_entities; - // } + if 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(), + ); + } } }); diff --git a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs index 83730d36ef8d..9ba2ff8882e1 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs @@ -1,5 +1,44 @@ mod edges; mod nodes; -pub use edges::EdgesVisualizer; -pub use nodes::NodeVisualizer; +pub use edges::{EdgeData, EdgesVisualizer}; +pub use nodes::{NodeData, NodeVisualizer}; +use re_chunk::EntityPath; + +use crate::graph::NodeIndex; + +/// Gathers all nodes, explicit and implicit, from the visualizers. +/// +/// Explicit nodes are nodes that are defined through the `GraphNode` archetype. +/// Implicit nodes are nodes that _only_ appear in edges defined by the `GraphEdge` archetype. +pub fn all_nodes<'a>( + nodes: impl IntoIterator, + edges: impl IntoIterator, +) -> impl Iterator { + let explicit = nodes + .into_iter() + .flat_map(|(entity, data)| data.nodes.iter().map(move |n| (entity, n.index))); + + let implicit = edges.into_iter().flat_map(|(entity, data)| { + data.edges.iter().flat_map(move |edge| { + edge.nodes() + .map(move |n| (entity, NodeIndex::from_entity_node(entity, n))) + }) + }); + + explicit.chain(implicit) +} + +/// Gathers all edges as tuples of `NodeIndex` from the visualizer. +pub fn all_edges<'a>( + edges: impl IntoIterator, +) -> impl Iterator { + edges.into_iter().flat_map(|(entity, data)| { + data.edges.iter().map(move |edge| { + let source = NodeIndex::from_entity_node(entity, &edge.source); + let target = NodeIndex::from_entity_node(entity, &edge.target); + + (entity, (source, target)) + }) + }) +} From f6024144a684787363756da5d7dc0dc25b154ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Wed, 13 Nov 2024 08:59:05 +0100 Subject: [PATCH 03/12] WIP: compute individual layouts --- Cargo.lock | 2 +- .../re_space_view_graph/src/graph/mod.rs | 79 ++++++++++ .../viewer/re_space_view_graph/src/layout.rs | 95 ----------- .../re_space_view_graph/src/layout/mod.rs | 85 ++++++++++ .../re_space_view_graph/src/properties.rs | 14 +- .../re_space_view_graph/src/ui/canvas.rs | 17 +- .../re_space_view_graph/src/ui/draw/node.rs | 6 +- .../viewer/re_space_view_graph/src/ui/mod.rs | 4 +- .../re_space_view_graph/src/ui/state.rs | 50 +++++- crates/viewer/re_space_view_graph/src/view.rs | 147 ++++++------------ .../src/visualizers/mod.rs | 50 ++---- 11 files changed, 290 insertions(+), 259 deletions(-) delete mode 100644 crates/viewer/re_space_view_graph/src/layout.rs create mode 100644 crates/viewer/re_space_view_graph/src/layout/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3f3bc70b2642..2a5bf95504fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2421,7 +2421,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fjadra" version = "0.1.0" -source = "git+https://github.com/grtlr/fjadra.git#40027d0c4d83096142509cb54b349313643de5e4" +source = "git+https://github.com/grtlr/fjadra.git#79c2e6dccbc84c2823325cf7bcbcfaa5730064d0" [[package]] name = "flatbuffers" diff --git a/crates/viewer/re_space_view_graph/src/graph/mod.rs b/crates/viewer/re_space_view_graph/src/graph/mod.rs index d38426bd15dc..86fe8ef4de4a 100644 --- a/crates/viewer/re_space_view_graph/src/graph/mod.rs +++ b/crates/viewer/re_space_view_graph/src/graph/mod.rs @@ -2,3 +2,82 @@ mod hash; pub(crate) use hash::GraphNodeHash; mod index; pub(crate) use index::NodeIndex; + +use re_types::components::{GraphNode, GraphType}; + +use crate::{ + types::{EdgeInstance, NodeInstance}, + visualizers::{EdgeData, NodeData}, +}; + +pub struct NodeInstanceImplicit { + pub node: GraphNode, + pub index: NodeIndex, +} + +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.rs b/crates/viewer/re_space_view_graph/src/layout.rs deleted file mode 100644 index 8ced23adfffd..000000000000 --- a/crates/viewer/re_space_view_graph/src/layout.rs +++ /dev/null @@ -1,95 +0,0 @@ -use egui::Rect; -use fjadra::{Center, Link, ManyBody, PositionX, PositionY, SimulationBuilder}; -use re_chunk::{EntityPath, TimeInt, Timeline}; - -use crate::{ - graph::NodeIndex, - ui::bounding_rect_from_iter, - visualizers::{all_edges, all_nodes, EdgeData, NodeData}, -}; - -/// Used to determine if a layout is up-to-date or outdated. -#[derive(Debug, PartialEq, Eq)] -pub struct Timestamp { - timeline: Timeline, - time: TimeInt, -} - -pub struct Layout { - valid_at: Timestamp, - extents: ahash::HashMap, -} - -impl std::fmt::Debug for Layout { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Layout") - .field("valid_at", &self.valid_at) - .finish() - } -} - -impl Layout { - pub fn needs_update(&self, timeline: Timeline, time: TimeInt) -> bool { - self.valid_at.timeline != timeline || self.valid_at.time != time - } - - pub fn bounding_rect(&self) -> Rect { - bounding_rect_from_iter(self.extents.values()) - } - - pub fn get(&self, node: &NodeIndex) -> Option<&Rect> { - self.extents.get(node) - } - - pub fn update(&mut self, node: &NodeIndex, rect: Rect) { - *self - .extents - .get_mut(node) - .expect("node should exist in layout") = rect; - } -} - -pub struct LayoutProvider; - -impl LayoutProvider { - pub fn compute<'a>( - timeline: Timeline, - time: TimeInt, - nodes: impl IntoIterator, - edges: impl IntoIterator + Clone, - ) -> Layout { - // Will hold the positions of the nodes, stored as bounding rectangles. - let mut extents = ahash::HashMap::default(); - - let nodes = all_nodes(nodes, edges.clone()) - .map(|n| n.1) - .collect::>(); - let node_index: ahash::HashMap = - nodes.iter().enumerate().map(|(i, n)| (*n, i)).collect(); - - let edges = all_edges(edges) - .map(|(_, (source, target))| (node_index[&source], node_index[&target])); - - let mut simulation = SimulationBuilder::default() - .build(nodes.iter().map(|_| Option::<[f64; 2]>::None)) - .add_force("link", Link::new(edges)) - .add_force("charge", ManyBody::new().strength(-300.0)) - .add_force("x", PositionX::new()) - .add_force("y", PositionY::new()); - - let positions = simulation.iter().last().expect("simulation should run"); - for (node, i) in node_index { - extents.entry(node).or_insert_with(|| { - let pos = positions[i]; - let pos = egui::Pos2::new(pos[0] as f32, pos[1] as f32); - let size = egui::Vec2::ZERO; - egui::Rect::from_min_size(pos, size) - }); - } - - Layout { - valid_at: Timestamp { timeline, time }, - extents, - } - } -} 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..9d80b911896f --- /dev/null +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -0,0 +1,85 @@ +use egui::{Pos2, Rect, Vec2}; +use fjadra as fj; + +use crate::{ + graph::{Graph, NodeIndex}, + types::NodeInstance, + ui::bounding_rect_from_iter, +}; + +#[derive(Debug)] +pub struct Layout { + extents: ahash::HashMap, +} + +impl Layout { + pub fn bounding_rect(&self) -> Rect { + bounding_rect_from_iter(self.extents.values().copied()) + } + + pub fn get(&self, node: &NodeIndex) -> Option { + self.extents.get(node).copied() + } + + pub fn update(&mut self, node: &NodeIndex, rect: Rect) { + *self + .extents + .get_mut(node) + .expect("node should exist in layout") = rect; + } +} + +impl<'a> From<&'a NodeInstance> for fj::Node { + fn from(instance: &'a NodeInstance) -> Self { + let mut node = fj::Node::default(); + if let Some(pos) = instance.position { + node = node.fixed_position(pos.x as f64, pos.y as f64); + } + node + } +} + +pub struct ForceLayout; + +impl ForceLayout { + pub fn compute<'a>(graph: &Graph<'a>) -> Layout { + let explicit = graph.nodes_explicit().map(|n| (n.index, fj::Node::from(n))); + let implicit = graph + .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 = graph + .edges() + .map(|e| (node_index[&e.source_index], node_index[&e.target_index])); + + let mut simulation = fj::SimulationBuilder::default() + .build(all_nodes) + .add_force("link", fj::Link::new(all_edges)) + .add_force("charge", fj::ManyBody::new().strength(-300.0)) + .add_force("x", fj::PositionX::new()) + .add_force("y", fj::PositionY::new()); + + let positions = simulation.iter().last().expect("simulation should run"); + + let extents = node_index + .into_iter() + .map(|(n, i)| { + let [x, y] = positions[i]; + let pos = Pos2::new(x as f32, y as f32); + (n, Rect::from_center_size(pos, Vec2::ZERO)) + }) + .collect(); + + Layout { extents } + } +} diff --git a/crates/viewer/re_space_view_graph/src/properties.rs b/crates/viewer/re_space_view_graph/src/properties.rs index bbc13844e535..1c5ea104bdc5 100644 --- a/crates/viewer/re_space_view_graph/src/properties.rs +++ b/crates/viewer/re_space_view_graph/src/properties.rs @@ -13,17 +13,9 @@ impl TypedComponentFallbackProvider for GraphSpaceView { return VisualBounds2D::default(); }; - let Some(layout) = &state.layout else { - return VisualBounds2D::default(); - }; - - let default_scene_rect = layout.bounding_rect(); - - if valid_bound(&default_scene_rect) { - default_scene_rect.into() - } else { - // Nothing in scene, probably. - VisualBounds2D::default() + match state.layout.bounding_rect() { + Some(rect) if valid_bound(&rect) => rect.into(), + _ => VisualBounds2D::default(), } } } diff --git a/crates/viewer/re_space_view_graph/src/ui/canvas.rs b/crates/viewer/re_space_view_graph/src/ui/canvas.rs index ec82c6442f21..b46a7c60f0df 100644 --- a/crates/viewer/re_space_view_graph/src/ui/canvas.rs +++ b/crates/viewer/re_space_view_graph/src/ui/canvas.rs @@ -10,7 +10,7 @@ use re_types::{ use re_viewer_context::SpaceViewHighlights; use std::hash::Hash; -use crate::types::{EdgeInstance, NodeInstance}; +use crate::{graph::NodeInstanceImplicit, types::{EdgeInstance, NodeInstance}}; use super::draw::{draw_edge, draw_entity, draw_explicit, draw_implicit}; @@ -178,17 +178,6 @@ pub struct Canvas<'a> { } impl<'a> Canvas<'a> { - /// Try to estimate a good starting `Rect` for a node that has not been layed out yet. - pub fn initial_rect(&self, node: &NodeInstance) -> Rect { - let size = node - .radius - .map(|r| self.context.radius_to_world(r)) - .unwrap_or(0.0) - * 2.0; - let pos = node.position.unwrap_or(Pos2::ZERO); - Rect::from_center_size(pos, Vec2::splat(size)) - } - /// Draws a regular node, i.e. an explicit node instance. pub fn explicit_node(&mut self, pos: Pos2, node: &NodeInstance) -> Response { self.node_wrapper(node.index, pos, |ui, world_to_ui| { @@ -196,8 +185,8 @@ impl<'a> Canvas<'a> { }) } - pub fn implicit_node(&mut self, pos: Pos2, node: &GraphNode) -> Response { - self.node_wrapper(node, pos, |ui, _| draw_implicit(ui, node)) + 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. 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 index 6f0c2fc6794c..5cff09e18f55 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw/node.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw/node.rs @@ -1,7 +1,7 @@ use egui::{Frame, Label, Response, RichText, Sense, Stroke, TextWrapMode, Ui, Vec2}; use re_types::components::GraphNode; -use crate::{types::NodeInstance, ui::canvas::CanvasContext}; +use crate::{graph::NodeInstanceImplicit, types::NodeInstance, ui::canvas::CanvasContext}; /// The `world_to_ui_scale` parameter is used to convert between world and ui coordinates. pub fn draw_explicit(ui: &mut Ui, ctx: &CanvasContext, node: &NodeInstance) -> Response { @@ -47,7 +47,7 @@ pub fn draw_explicit(ui: &mut Ui, ctx: &CanvasContext, node: &NodeInstance) -> R } /// Draws an implicit node instance (dummy node). -pub fn draw_implicit(ui: &mut egui::Ui, node: &GraphNode) -> Response { +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; @@ -63,5 +63,5 @@ pub fn draw_implicit(ui: &mut egui::Ui, node: &GraphNode) -> Response { }) }) .response - .on_hover_text(format!("Implicit Node: `{}`", node.as_str(),)) + .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 index bfab476062b5..b27bc0da96d3 100644 --- a/crates/viewer/re_space_view_graph/src/ui/mod.rs +++ b/crates/viewer/re_space_view_graph/src/ui/mod.rs @@ -5,6 +5,6 @@ pub mod canvas; pub use state::GraphSpaceViewState; -pub fn bounding_rect_from_iter<'a>(rectangles: impl Iterator) -> egui::Rect { - rectangles.fold(egui::Rect::NOTHING, |acc, rect| acc.union(*rect)) +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/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index c1f0ee0e3d7d..65eedf2ce0dd 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -1,3 +1,6 @@ +use ahash::HashMap; +use egui::Rect; +use re_chunk::{EntityPath, TimeInt, Timeline}; use re_format::format_f32; use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; @@ -12,7 +15,7 @@ use super::bounding_rect_from_iter; /// This state is preserved between frames, but not across Viewer sessions. #[derive(Default)] pub struct GraphSpaceViewState { - pub layout: Option, + pub layout: LayoutState, pub show_debug: bool, @@ -21,14 +24,14 @@ pub struct GraphSpaceViewState { impl GraphSpaceViewState { pub fn layout_ui(&mut self, ui: &mut egui::Ui) { - let Some(layout) = self.layout.as_ref() else { + let Some(rect) = self.layout.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 } = layout.bounding_rect(); + 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),)); }); @@ -51,3 +54,44 @@ impl SpaceViewState for GraphSpaceViewState { self } } + +/// Used to determine if a layout is up-to-date or outdated. +#[derive(Debug, PartialEq, Eq)] +pub struct Timestamp { + timeline: Timeline, + time: TimeInt, +} + +/// 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, + Outdated { + timestamp: Timestamp, + layouts: HashMap, + }, + Finished { + timestamp: Timestamp, + layouts: HashMap, + }, +} + +impl LayoutState { + pub fn bounding_rect(&self) -> Option { + match self { + Self::Outdated { layouts, .. } | Self::Finished { layouts, .. } => { + let union_rect = + bounding_rect_from_iter(layouts.values().map(|l| l.bounding_rect())); + Some(union_rect) + } + Self::None => None, + } + } + + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } +} diff --git a/crates/viewer/re_space_view_graph/src/view.rs b/crates/viewer/re_space_view_graph/src/view.rs index ad5fdbdea91c..4cbef95e6a99 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -1,9 +1,5 @@ -use std::collections::{BTreeSet, HashSet}; +use egui::{self}; -use ahash::HashMap; -use egui::{self, Vec2}; - -use re_log::external::log; use re_log_types::EntityPath; use re_space_view::view_property_ui; use re_types::{ @@ -21,10 +17,10 @@ use re_viewer_context::{ use re_viewport_blueprint::ViewProperty; use crate::{ - graph::NodeIndex, - layout::LayoutProvider, - ui::{bounding_rect_from_iter, canvas::CanvasBuilder, GraphSpaceViewState}, - visualizers::{EdgesVisualizer, NodeVisualizer}, + graph::Graph, + layout::ForceLayout, + ui::{canvas::CanvasBuilder, GraphSpaceViewState}, + visualizers::{merge, EdgesVisualizer, NodeVisualizer}, }; #[derive(Default)] @@ -64,22 +60,19 @@ impl SpaceViewClass for GraphSpaceView { fn preferred_tile_aspect_ratio(&self, state: &dyn SpaceViewState) -> Option { let state = state.downcast_ref::().ok()?; - let layout = state.layout.as_ref()?; - - let (width, height) = state.world_bounds.map_or_else( - || { - let bbox = layout.bounding_rect(); - (bbox.width().abs(), bbox.height().abs()) - }, - |bounds| { - ( - bounds.x_range.abs_len() as f32, - bounds.y_range.abs_len() as f32, - ) - }, - ); - Some(width / height) + 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.bounding_rect() { + let width = rect.width().abs(); + let height = rect.height().abs(); + return Some(width / height); + } + None } // TODO(grtlr): implement `recommended_root_for_entities` @@ -138,11 +131,8 @@ impl SpaceViewClass for GraphSpaceView { let node_data = &system_output.view_systems.get::()?.data; let edge_data = &system_output.view_systems.get::()?.data; - // We need to sort the entities to ensure that we are always drawing them in the right order. - let entities = node_data - .keys() - .chain(edge_data.keys()) - .collect::>(); + let graphs = + merge(node_data, edge_data).map(|(ent, nodes, edges)| (ent, Graph::new(nodes, edges))); let state = state.downcast_mut::()?; @@ -157,24 +147,6 @@ impl SpaceViewClass for GraphSpaceView { let layout_was_empty = state.layout.is_none(); - let layout = match &mut state.layout { - Some(layout) if !layout.needs_update(query.timeline, query.latest_at) => { - layout // Layout is up to date, reuse it. - } - _ => { - log::debug!("Recomputing graph layout"); - - let layout = LayoutProvider::compute( - query.timeline, - query.latest_at, - node_data.iter(), - edge_data.iter(), - ); - - state.layout.insert(layout) - } - }; - state.world_bounds = Some(bounds); let bounds_rect: egui::Rect = bounds.into(); @@ -185,63 +157,44 @@ impl SpaceViewClass for GraphSpaceView { viewer.show_debug(); } - let mut seen: HashSet = HashSet::new(); - let (new_world_bounds, response) = viewer.canvas(ui, |mut scene| { - // We store the offset to draw entities next to each other. - // This is a workaround and will probably be removed once we have auto-layout. - let mut entity_offset = egui::Vec2::ZERO; - - for entity in entities { - // We keep track of the size of the current entity. - - let mut entity_rect = egui::Rect::NOTHING; - if let Some(data) = node_data.get(entity) { - for node in &data.nodes { - seen.insert(node.index); - let pos = layout - .get(&node.index) - .expect("explicit node should be in layout"); - let response = scene.explicit_node(pos.min, node); - layout.update(&node.index, response.rect); - entity_rect = entity_rect.union(response.rect); - } + for (entity, graph) in graphs { + // We compute the layout once to find good starting positions for the nodes. + let mut layout = ForceLayout::compute(&graph); + + // Draw explicit nodes. + for node in graph.nodes_explicit() { + let pos = layout + .get(&node.index) + .expect("explicit node should be in layout"); + let response = scene.explicit_node(pos.min, node); + layout.update(&node.index, response.rect); } - if let Some(data) = edge_data.get(entity) { - // An implicit node is a node that is not explicitly specified in the `GraphNodes` archetype. - let implicit_nodes = data - .edges - .iter() - .flat_map(|e| e.nodes()) - .filter(|n| !seen.contains(&NodeIndex::from_entity_node(entity, n))) - .collect::>(); - - for node in implicit_nodes { - let ix = NodeIndex::from_entity_node(entity, node); - seen.insert(ix); - let current = layout.get(&ix).expect("implicit node should be in layout"); - let response = scene.implicit_node(current.min, node); - layout.update(&ix, response.rect); - entity_rect = entity_rect.union(response.rect); - } + // Draw implicit nodes. + for node in graph.nodes_implicit() { + let current = layout + .get(&node.index) + .expect("implicit node should be in layout"); + let response = scene.implicit_node(current.min, node); + layout.update(&node.index, response.rect); + } - for edge in &data.edges { - if let (Some(source_pos), Some(target_pos)) = - (layout.get(&edge.source_index), layout.get(&edge.target_index)) - { - scene.edge( - source_pos.translate(entity_offset), - target_pos.translate(entity_offset), - edge, - data.graph_type == components::GraphType::Directed, - ); - } + // 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); } } - if entity_rect.is_positive() { - let response = scene.entity(entity, entity_rect, &query.highlights); + // Draw entity rect. + let rect = layout.bounding_rect(); + if rect.is_positive() { + let response = scene.entity(entity, rect, &query.highlights); let instance_path = InstancePath::entity_all(entity.clone()); ctx.select_hovered_on_click( diff --git a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs index 9ba2ff8882e1..d2d655431c9e 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs @@ -1,44 +1,28 @@ mod edges; mod nodes; +use std::collections::BTreeSet; + pub use edges::{EdgeData, EdgesVisualizer}; pub use nodes::{NodeData, NodeVisualizer}; -use re_chunk::EntityPath; - -use crate::graph::NodeIndex; -/// Gathers all nodes, explicit and implicit, from the visualizers. -/// -/// Explicit nodes are nodes that are defined through the `GraphNode` archetype. -/// Implicit nodes are nodes that _only_ appear in edges defined by the `GraphEdge` archetype. -pub fn all_nodes<'a>( - nodes: impl IntoIterator, - edges: impl IntoIterator, -) -> impl Iterator { - let explicit = nodes - .into_iter() - .flat_map(|(entity, data)| data.nodes.iter().map(move |n| (entity, n.index))); - - let implicit = edges.into_iter().flat_map(|(entity, data)| { - data.edges.iter().flat_map(move |edge| { - edge.nodes() - .map(move |n| (entity, NodeIndex::from_entity_node(entity, n))) - }) - }); +use re_chunk::EntityPath; - explicit.chain(implicit) -} +/// 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 { -/// Gathers all edges as tuples of `NodeIndex` from the visualizer. -pub fn all_edges<'a>( - edges: impl IntoIterator, -) -> impl Iterator { - edges.into_iter().flat_map(|(entity, data)| { - data.edges.iter().map(move |edge| { - let source = NodeIndex::from_entity_node(entity, &edge.source); - let target = NodeIndex::from_entity_node(entity, &edge.target); + // 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::>(); - (entity, (source, target)) - }) + unique_entities.into_iter().map(|entity| { + let nodes = node_data.get(entity); + let edges = edge_data.get(entity); + (entity, nodes, edges) }) } From 2018c1a6324fa59012a880585b27d7dbafc4f55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Wed, 13 Nov 2024 11:41:46 +0100 Subject: [PATCH 04/12] WIP: restructure --- .../re_space_view_graph/src/graph/hash.rs | 2 +- .../re_space_view_graph/src/graph/index.rs | 2 +- .../re_space_view_graph/src/graph/mod.rs | 5 +--- .../re_space_view_graph/src/layout/mod.rs | 8 +++--- crates/viewer/re_space_view_graph/src/lib.rs | 1 - .../viewer/re_space_view_graph/src/types.rs | 28 ------------------- .../re_space_view_graph/src/ui/canvas.rs | 7 ++--- .../re_space_view_graph/src/ui/draw/node.rs | 3 +- .../src/visualizers/edges.rs | 9 +++++- .../src/visualizers/mod.rs | 5 ++-- .../src/visualizers/nodes.rs | 10 ++++++- 11 files changed, 29 insertions(+), 51 deletions(-) delete mode 100644 crates/viewer/re_space_view_graph/src/types.rs diff --git a/crates/viewer/re_space_view_graph/src/graph/hash.rs b/crates/viewer/re_space_view_graph/src/graph/hash.rs index e15f8c103cc4..4d8e430de571 100644 --- a/crates/viewer/re_space_view_graph/src/graph/hash.rs +++ b/crates/viewer/re_space_view_graph/src/graph/hash.rs @@ -3,7 +3,7 @@ use re_types::components; /// A 64 bit hash of [`components::GraphNode`] with very small risk of collision. #[derive(Copy, Clone, Eq, PartialOrd, Ord)] -pub(crate) struct GraphNodeHash(Hash64); +pub struct GraphNodeHash(Hash64); impl nohash_hasher::IsEnabled for GraphNodeHash {} diff --git a/crates/viewer/re_space_view_graph/src/graph/index.rs b/crates/viewer/re_space_view_graph/src/graph/index.rs index 11ba80da6a25..10db2dcf7d0d 100644 --- a/crates/viewer/re_space_view_graph/src/graph/index.rs +++ b/crates/viewer/re_space_view_graph/src/graph/index.rs @@ -4,7 +4,7 @@ use re_types::components; use super::GraphNodeHash; #[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) struct NodeIndex { +pub struct NodeIndex { pub entity_hash: EntityPathHash, pub node_hash: GraphNodeHash, } diff --git a/crates/viewer/re_space_view_graph/src/graph/mod.rs b/crates/viewer/re_space_view_graph/src/graph/mod.rs index 86fe8ef4de4a..070f3e259490 100644 --- a/crates/viewer/re_space_view_graph/src/graph/mod.rs +++ b/crates/viewer/re_space_view_graph/src/graph/mod.rs @@ -5,10 +5,7 @@ pub(crate) use index::NodeIndex; use re_types::components::{GraphNode, GraphType}; -use crate::{ - types::{EdgeInstance, NodeInstance}, - visualizers::{EdgeData, NodeData}, -}; +use crate::visualizers::{EdgeData, EdgeInstance, NodeData, NodeInstance}; pub struct NodeInstanceImplicit { pub node: GraphNode, diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index 9d80b911896f..620cd863718f 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -3,11 +3,11 @@ use fjadra as fj; use crate::{ graph::{Graph, NodeIndex}, - types::NodeInstance, ui::bounding_rect_from_iter, + visualizers::NodeInstance, }; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct Layout { extents: ahash::HashMap, } @@ -31,7 +31,7 @@ impl Layout { impl<'a> From<&'a NodeInstance> for fj::Node { fn from(instance: &'a NodeInstance) -> Self { - let mut node = fj::Node::default(); + let mut node = Self::default(); if let Some(pos) = instance.position { node = node.fixed_position(pos.x as f64, pos.y as f64); } @@ -42,7 +42,7 @@ impl<'a> From<&'a NodeInstance> for fj::Node { pub struct ForceLayout; impl ForceLayout { - pub fn compute<'a>(graph: &Graph<'a>) -> Layout { + pub fn compute(graph: &Graph<'_>) -> Layout { let explicit = graph.nodes_explicit().map(|n| (n.index, fj::Node::from(n))); let implicit = graph .nodes_implicit() diff --git a/crates/viewer/re_space_view_graph/src/lib.rs b/crates/viewer/re_space_view_graph/src/lib.rs index 5e05c7cd698c..17634033ab30 100644 --- a/crates/viewer/re_space_view_graph/src/lib.rs +++ b/crates/viewer/re_space_view_graph/src/lib.rs @@ -5,7 +5,6 @@ mod graph; mod layout; mod properties; -mod types; mod ui; mod view; mod visualizers; diff --git a/crates/viewer/re_space_view_graph/src/types.rs b/crates/viewer/re_space_view_graph/src/types.rs deleted file mode 100644 index 88362a2178dc..000000000000 --- a/crates/viewer/re_space_view_graph/src/types.rs +++ /dev/null @@ -1,28 +0,0 @@ -use re_types::{ - components::{self, GraphNode}, - ArrowString, -}; - -use crate::graph::NodeIndex; - -pub struct NodeInstance { - pub node: components::GraphNode, - pub index: NodeIndex, - pub label: Option, - pub color: Option, - pub position: Option, - pub radius: Option, -} - -pub struct EdgeInstance { - pub source: GraphNode, - pub target: GraphNode, - pub source_index: NodeIndex, - pub target_index: NodeIndex, -} - -impl EdgeInstance { - pub fn nodes(&self) -> impl Iterator { - [&self.source, &self.target].into_iter() - } -} diff --git a/crates/viewer/re_space_view_graph/src/ui/canvas.rs b/crates/viewer/re_space_view_graph/src/ui/canvas.rs index b46a7c60f0df..c2299c7b0ce2 100644 --- a/crates/viewer/re_space_view_graph/src/ui/canvas.rs +++ b/crates/viewer/re_space_view_graph/src/ui/canvas.rs @@ -3,14 +3,11 @@ use egui::{ Stroke, Ui, Vec2, }; use re_chunk::EntityPath; -use re_types::{ - components::{GraphNode, Radius}, - datatypes::Float32, -}; +use re_types::{components::Radius, datatypes::Float32}; use re_viewer_context::SpaceViewHighlights; use std::hash::Hash; -use crate::{graph::NodeInstanceImplicit, types::{EdgeInstance, NodeInstance}}; +use crate::{graph::NodeInstanceImplicit, visualizers::{EdgeInstance, NodeInstance}}; use super::draw::{draw_edge, draw_entity, 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 index 5cff09e18f55..5add370564a5 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw/node.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw/node.rs @@ -1,7 +1,6 @@ use egui::{Frame, Label, Response, RichText, Sense, Stroke, TextWrapMode, Ui, Vec2}; -use re_types::components::GraphNode; -use crate::{graph::NodeInstanceImplicit, types::NodeInstance, ui::canvas::CanvasContext}; +use crate::{graph::NodeInstanceImplicit, ui::canvas::CanvasContext, 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: &CanvasContext, node: &NodeInstance) -> Response { diff --git a/crates/viewer/re_space_view_graph/src/visualizers/edges.rs b/crates/viewer/re_space_view_graph/src/visualizers/edges.rs index aeccc9eee326..f082b2b9178c 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/edges.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/edges.rs @@ -11,13 +11,20 @@ use re_viewer_context::{ ViewQuery, ViewSystemIdentifier, VisualizerQueryInfo, VisualizerSystem, }; -use crate::{graph::NodeIndex, types::EdgeInstance}; +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, +} + pub struct EdgeData { pub graph_type: components::GraphType, pub edges: Vec, diff --git a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs index d2d655431c9e..0f91ee66492e 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/mod.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/mod.rs @@ -3,8 +3,8 @@ mod nodes; use std::collections::BTreeSet; -pub use edges::{EdgeData, EdgesVisualizer}; -pub use nodes::{NodeData, NodeVisualizer}; +pub use edges::{EdgeData, EdgeInstance, EdgesVisualizer}; +pub use nodes::{NodeData, NodeInstance, NodeVisualizer}; use re_chunk::EntityPath; @@ -13,7 +13,6 @@ 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() diff --git a/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs b/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs index 4555c213015b..67fda808968b 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/nodes.rs @@ -16,13 +16,21 @@ use re_viewer_context::{ }; use crate::graph::NodeIndex; -use crate::types::NodeInstance; #[derive(Default)] pub struct NodeVisualizer { pub data: ahash::HashMap, } +pub struct NodeInstance { + pub node: components::GraphNode, + pub index: NodeIndex, + pub label: Option, + pub color: Option, + pub position: Option, + pub radius: Option, +} + pub struct NodeData { pub nodes: Vec, } From 552d3c5ddb06d960fa59f44f9f6901ccf4945193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Thu, 14 Nov 2024 10:12:31 +0100 Subject: [PATCH 05/12] WIP: finish initial version of auto-layout --- .../re_space_view_graph/src/layout/mod.rs | 17 ++--- .../re_space_view_graph/src/ui/state.rs | 69 ++++++++++++++----- crates/viewer/re_space_view_graph/src/view.rs | 20 +++--- .../src/visualizers/edges.rs | 2 +- 4 files changed, 74 insertions(+), 34 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index 620cd863718f..c243a1f64546 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -42,11 +42,11 @@ impl<'a> From<&'a NodeInstance> for fj::Node { pub struct ForceLayout; impl ForceLayout { - pub fn compute(graph: &Graph<'_>) -> Layout { - let explicit = graph.nodes_explicit().map(|n| (n.index, fj::Node::from(n))); - let implicit = graph - .nodes_implicit() - .map(|n| (n.index, fj::Node::default())); + pub fn compute<'a>(graphs: impl Iterator> + Clone) -> Layout { + 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 @@ -58,9 +58,10 @@ impl ForceLayout { }) .collect(); - let all_edges = graph - .edges() - .map(|e| (node_index[&e.source_index], node_index[&e.target_index])); + let all_edges = graphs.flat_map(|g| { + g.edges() + .map(|e| (node_index[&e.source_index], node_index[&e.target_index])) + }); let mut simulation = fj::SimulationBuilder::default() .build(all_nodes) diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 65eedf2ce0dd..1536d499a6d9 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -1,14 +1,15 @@ -use ahash::HashMap; use egui::Rect; -use re_chunk::{EntityPath, TimeInt, Timeline}; +use re_chunk::{TimeInt, Timeline}; use re_format::format_f32; +use re_log::external::log; use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; use re_viewer_context::SpaceViewState; -use crate::layout::Layout; - -use super::bounding_rect_from_iter; +use crate::{ + graph::Graph, + layout::{ForceLayout, Layout}, +}; /// Space view state for the custom space view. /// @@ -62,6 +63,12 @@ pub struct Timestamp { time: TimeInt, } +impl Timestamp { + pub fn new(timeline: Timeline, time: TimeInt) -> Self { + Self { timeline, time } + } +} + /// 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. @@ -69,24 +76,16 @@ pub struct Timestamp { pub enum LayoutState { #[default] None, - Outdated { - timestamp: Timestamp, - layouts: HashMap, - }, - Finished { + Current { timestamp: Timestamp, - layouts: HashMap, + layout: Layout, }, } impl LayoutState { pub fn bounding_rect(&self) -> Option { match self { - Self::Outdated { layouts, .. } | Self::Finished { layouts, .. } => { - let union_rect = - bounding_rect_from_iter(layouts.values().map(|l| l.bounding_rect())); - Some(union_rect) - } + Self::Current { layout, .. } => Some(layout.bounding_rect()), Self::None => None, } } @@ -94,4 +93,42 @@ impl LayoutState { pub fn is_none(&self) -> bool { matches!(self, Self::None) } + + pub fn needs_update(&self, timeline: Timeline, time: TimeInt) -> bool { + match self { + Self::Current { timestamp, .. } => timestamp != &Timestamp { timeline, time }, + Self::None => true, + } + } + + /// This method is lazy. A new layout is only computed if the current timestamp requires it. + pub fn get_or_compute<'a>( + &'a mut self, + timeline: Timeline, + time: TimeInt, + graphs: impl Iterator> + Clone, + ) -> &'a mut Layout { + let requested = Timestamp::new(timeline, time); + + // Check if we need to update, and if not, return the current layout. + // The complexity of the logic here is due to the borrow checker. + if matches!(self, Self::Current { timestamp, .. } if timestamp == &requested) { + return match self { + Self::Current { layout, .. } => layout, + _ => unreachable!(), // We just checked that the state is `Self::Current`. + }; + } + + let layout = ForceLayout::compute(graphs); + + *self = Self::Current { + timestamp: requested, + layout, + }; + + match self { + Self::Current { layout, .. } => layout, + _ => 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 index 4cbef95e6a99..fc613555255b 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -1,4 +1,4 @@ -use egui::{self}; +use egui::{self, emath::TSTransform}; use re_log_types::EntityPath; use re_space_view::view_property_ui; @@ -18,7 +18,6 @@ use re_viewport_blueprint::ViewProperty; use crate::{ graph::Graph, - layout::ForceLayout, ui::{canvas::CanvasBuilder, GraphSpaceViewState}, visualizers::{merge, EdgesVisualizer, NodeVisualizer}, }; @@ -131,8 +130,9 @@ impl SpaceViewClass for GraphSpaceView { 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))); + let graphs = merge(node_data, edge_data) + .map(|(ent, nodes, edges)| (ent, Graph::new(nodes, edges))) + .collect::>(); let state = state.downcast_mut::()?; @@ -146,6 +146,11 @@ impl SpaceViewClass for GraphSpaceView { bounds_property.component_or_fallback(ctx, self, state)?; let layout_was_empty = state.layout.is_none(); + let layout = state.layout.get_or_compute( + query.timeline, + query.latest_at, + graphs.iter().map(|(ent, graph)| graph), + ); state.world_bounds = Some(bounds); let bounds_rect: egui::Rect = bounds.into(); @@ -158,10 +163,7 @@ impl SpaceViewClass for GraphSpaceView { } let (new_world_bounds, response) = viewer.canvas(ui, |mut scene| { - for (entity, graph) in graphs { - // We compute the layout once to find good starting positions for the nodes. - let mut layout = ForceLayout::compute(&graph); - + for (entity, graph) in &graphs { // Draw explicit nodes. for node in graph.nodes_explicit() { let pos = layout @@ -196,7 +198,7 @@ impl SpaceViewClass for GraphSpaceView { if rect.is_positive() { let response = scene.entity(entity, rect, &query.highlights); - let instance_path = InstancePath::entity_all(entity.clone()); + let instance_path = InstancePath::entity_all((*entity).clone()); ctx.select_hovered_on_click( &response, vec![(Item::DataResult(query.space_view_id, instance_path), None)] diff --git a/crates/viewer/re_space_view_graph/src/visualizers/edges.rs b/crates/viewer/re_space_view_graph/src/visualizers/edges.rs index f082b2b9178c..14ff4cdede63 100644 --- a/crates/viewer/re_space_view_graph/src/visualizers/edges.rs +++ b/crates/viewer/re_space_view_graph/src/visualizers/edges.rs @@ -11,7 +11,7 @@ use re_viewer_context::{ ViewQuery, ViewSystemIdentifier, VisualizerQueryInfo, VisualizerSystem, }; -use crate::{graph::NodeIndex}; +use crate::graph::NodeIndex; #[derive(Default)] pub struct EdgesVisualizer { From efa710b156fc2db7d1c15a630cb614177dfaaf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Thu, 14 Nov 2024 10:15:22 +0100 Subject: [PATCH 06/12] WIP: fmt --- crates/viewer/re_space_view_graph/src/layout/mod.rs | 10 ++++++---- crates/viewer/re_space_view_graph/src/ui/canvas.rs | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index c243a1f64546..92cf20574d8b 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -43,10 +43,12 @@ pub struct ForceLayout; impl ForceLayout { pub fn compute<'a>(graphs: impl Iterator> + Clone) -> Layout { - 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 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 diff --git a/crates/viewer/re_space_view_graph/src/ui/canvas.rs b/crates/viewer/re_space_view_graph/src/ui/canvas.rs index c2299c7b0ce2..c49bab9e1cc4 100644 --- a/crates/viewer/re_space_view_graph/src/ui/canvas.rs +++ b/crates/viewer/re_space_view_graph/src/ui/canvas.rs @@ -7,7 +7,10 @@ use re_types::{components::Radius, datatypes::Float32}; use re_viewer_context::SpaceViewHighlights; use std::hash::Hash; -use crate::{graph::NodeInstanceImplicit, visualizers::{EdgeInstance, NodeInstance}}; +use crate::{ + graph::NodeInstanceImplicit, + visualizers::{EdgeInstance, NodeInstance}, +}; use super::draw::{draw_edge, draw_entity, draw_explicit, draw_implicit}; From 2658d5eb9ed161076f55e2c9c93e7d78d53baf96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Thu, 14 Nov 2024 14:11:11 +0100 Subject: [PATCH 07/12] WIP: stash --- .../re_space_view_graph/src/layout/mod.rs | 38 +++--- .../re_space_view_graph/src/ui/state.rs | 39 +++---- crates/viewer/re_space_view_graph/src/view.rs | 2 +- pixi.lock | 108 +++++++----------- 4 files changed, 83 insertions(+), 104 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index 92cf20574d8b..8a385a5da145 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -39,10 +39,13 @@ impl<'a> From<&'a NodeInstance> for fj::Node { } } -pub struct ForceLayout; +pub struct ForceLayout { + simulation: fj::Simulation, + node_index: ahash::HashMap, +} impl ForceLayout { - pub fn compute<'a>(graphs: impl Iterator> + Clone) -> Layout { + 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)))); @@ -65,24 +68,31 @@ impl ForceLayout { .map(|e| (node_index[&e.source_index], node_index[&e.target_index])) }); - let mut simulation = fj::SimulationBuilder::default() + let simulation = fj::SimulationBuilder::default() .build(all_nodes) .add_force("link", fj::Link::new(all_edges)) - .add_force("charge", fj::ManyBody::new().strength(-300.0)) + .add_force("charge", fj::ManyBody::new()) .add_force("x", fj::PositionX::new()) .add_force("y", fj::PositionY::new()); - let positions = simulation.iter().last().expect("simulation should run"); + Self { + simulation, + node_index, + } + } - let extents = node_index - .into_iter() - .map(|(n, i)| { - let [x, y] = positions[i]; - let pos = Pos2::new(x as f32, y as f32); - (n, Rect::from_center_size(pos, Vec2::ZERO)) - }) - .collect(); + pub fn tick(&mut self, layout: &mut Layout) -> bool { + self.simulation.tick(1); + + let positions = self.simulation.positions().collect::>(); + + for (node, extent) in layout.extents.iter_mut() { + 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); + } - Layout { extents } + self.simulation.finished() } } diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 1536d499a6d9..6bbecab1e621 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -76,7 +76,12 @@ impl Timestamp { pub enum LayoutState { #[default] None, - Current { + InProgress { + timestamp: Timestamp, + layout: Layout, + provider: ForceLayout, + }, + Finished { timestamp: Timestamp, layout: Layout, }, @@ -85,7 +90,7 @@ pub enum LayoutState { impl LayoutState { pub fn bounding_rect(&self) -> Option { match self { - Self::Current { layout, .. } => Some(layout.bounding_rect()), + Self::Finished { layout, .. } => Some(layout.bounding_rect()), Self::None => None, } } @@ -94,15 +99,8 @@ impl LayoutState { matches!(self, Self::None) } - pub fn needs_update(&self, timeline: Timeline, time: TimeInt) -> bool { - match self { - Self::Current { timestamp, .. } => timestamp != &Timestamp { timeline, time }, - Self::None => true, - } - } - /// This method is lazy. A new layout is only computed if the current timestamp requires it. - pub fn get_or_compute<'a>( + pub fn update<'a>( &'a mut self, timeline: Timeline, time: TimeInt, @@ -110,24 +108,23 @@ impl LayoutState { ) -> &'a mut Layout { let requested = Timestamp::new(timeline, time); - // Check if we need to update, and if not, return the current layout. - // The complexity of the logic here is due to the borrow checker. - if matches!(self, Self::Current { timestamp, .. } if timestamp == &requested) { - return match self { - Self::Current { layout, .. } => layout, - _ => unreachable!(), // We just checked that the state is `Self::Current`. - }; + match self { + Self::Finished { timestamp, .. } if timestamp == &requested => { + return match self { + Self::Finished { layout, .. } => layout, + _ => unreachable!(), // We just checked that the state is `Self::Current`. + }; + }, + Self::Finished { .. } => (), // TODO(grtlr): repurpose old layout } - let layout = ForceLayout::compute(graphs); - - *self = Self::Current { + *self = Self::Finished { timestamp: requested, layout, }; match self { - Self::Current { layout, .. } => layout, + Self::Finished { layout, .. } => layout, _ => 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 index fc613555255b..9f46fea3894c 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -146,7 +146,7 @@ impl SpaceViewClass for GraphSpaceView { bounds_property.component_or_fallback(ctx, self, state)?; let layout_was_empty = state.layout.is_none(); - let layout = state.layout.get_or_compute( + let layout = state.layout.update( query.timeline, query.latest_at, graphs.iter().map(|(ent, graph)| graph), diff --git a/pixi.lock b/pixi.lock index bd9337dbedcc..6d57a5b83b2d 100644 --- a/pixi.lock +++ b/pixi.lock @@ -16956,13 +16956,13 @@ packages: sha256: 8ae055c0b8b0dd7757e4e666f6163172859044d4090830aecbec3460cdb318ee requires_dist: - accelerate - - diffusers==0.27.2 - - numpy - opencv-python - pillow - - rerun-sdk + - diffusers==0.27.2 + - numpy - torch==2.2.2 - transformers + - rerun-sdk requires_python: '>=3.10' editable: true - kind: pypi @@ -16972,13 +16972,13 @@ packages: sha256: 8ae055c0b8b0dd7757e4e666f6163172859044d4090830aecbec3460cdb318ee requires_dist: - accelerate - - opencv-python - - pillow - diffusers==0.27.2 - numpy + - opencv-python + - pillow + - rerun-sdk - torch==2.2.2 - transformers - - rerun-sdk requires_python: '>=3.10' editable: true - kind: pypi @@ -17392,7 +17392,7 @@ packages: - numpy - opencv-contrib-python>4.6 - pillow - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk - timm==0.9.11 - torch==2.2.2 @@ -17407,7 +17407,7 @@ packages: - 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 @@ -17422,9 +17422,9 @@ packages: - 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-mri @@ -19243,7 +19243,7 @@ packages: - mediapipe==0.10.9 ; sys_platform == 'darwin' - numpy - opencv-python>4.9 - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk - tqdm requires_python: <3.12 @@ -19258,7 +19258,7 @@ packages: - 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 @@ -20352,7 +20352,7 @@ packages: - mediapipe==0.10.9 ; sys_platform == 'darwin' - numpy - opencv-python>4.6 - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk requires_python: <3.12 editable: true @@ -20366,7 +20366,7 @@ packages: - 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 @@ -29275,30 +29275,6 @@ packages: - beautifulsoup4 ; extra == 'htmlsoup' - cython>=3.0.11 ; extra == 'source' requires_python: '>=3.6' -- kind: pypi - name: lxml - version: 5.3.0 - url: https://files.pythonhosted.org/packages/42/07/b29571a58a3a80681722ea8ed0ba569211d9bb8531ad49b5cacf6d409185/lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl - sha256: eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654 - requires_dist: - - cssselect>=0.7 ; extra == 'cssselect' - - html5lib ; extra == 'html5' - - lxml-html-clean ; extra == 'html-clean' - - beautifulsoup4 ; extra == 'htmlsoup' - - cython>=3.0.11 ; extra == 'source' - requires_python: '>=3.6' -- kind: pypi - name: lxml - version: 5.3.0 - url: https://files.pythonhosted.org/packages/5c/a8/449faa2a3cbe6a99f8d38dcd51a3ee8844c17862841a6f769ea7c2a9cd0f/lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl - sha256: 74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b - requires_dist: - - cssselect>=0.7 ; extra == 'cssselect' - - html5lib ; extra == 'html5' - - lxml-html-clean ; extra == 'html-clean' - - beautifulsoup4 ; extra == 'htmlsoup' - - cython>=3.0.11 ; extra == 'source' - requires_python: '>=3.6' - kind: pypi name: lxml version: 5.3.0 @@ -31060,6 +31036,14 @@ packages: requires_dist: - rerun-sdk editable: true +- kind: pypi + name: node-link + version: 0.1.0 + path: examples/python/node_link + sha256: df35fef8f84a2357b97253df002d9ac6aaecf21aeb5a1027be149bfef785579b + requires_dist: + - rerun-sdk + editable: true - kind: conda name: nodejs version: 20.17.0 @@ -31516,9 +31500,9 @@ packages: path: examples/python/nv12 sha256: c8ca97c5d8c04037cd5eb9a65be7b1e7d667c11d4dba3ee9aad5956ccf926dc4 requires_dist: - - numpy - - opencv-python - rerun-sdk>=0.10 + - opencv-python + - numpy editable: true - kind: pypi name: nv12 @@ -31526,9 +31510,9 @@ packages: path: examples/python/nv12 sha256: c8ca97c5d8c04037cd5eb9a65be7b1e7d667c11d4dba3ee9aad5956ccf926dc4 requires_dist: - - rerun-sdk>=0.10 - - opencv-python - numpy + - opencv-python + - rerun-sdk>=0.10 editable: true - kind: pypi name: nvidia-cublas-cu12 @@ -31619,7 +31603,7 @@ packages: - betterproto[compiler] - numpy - opencv-python>4.6 - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk - scipy editable: true @@ -31632,7 +31616,7 @@ packages: - betterproto[compiler] - numpy - opencv-python>4.6 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - scipy editable: true @@ -35154,7 +35138,7 @@ packages: sha256: 9006b1b7ca8bd9c90ba0bf0d7a00641b7dd13a6de76a2828f79ec5b853a4ef98 requires_dist: - numpy - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk - trimesh==3.15.2 editable: true @@ -35165,7 +35149,7 @@ packages: sha256: 9006b1b7ca8bd9c90ba0bf0d7a00641b7dd13a6de76a2828f79ec5b853a4ef98 requires_dist: - numpy - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - trimesh==3.15.2 editable: true @@ -35390,18 +35374,6 @@ packages: - jupyterlab ; extra == 'dev' - hatch ; extra == 'dev' editable: true -- kind: pypi - name: rerun-notebook - version: 0.20.0a1+dev - path: rerun_notebook - sha256: f20def987d6c97a658313841861dd7ce129fde8a658647756571844989b1451e - requires_dist: - - anywidget - - jupyter-ui-poll - - hatch ; extra == 'dev' - - jupyterlab ; extra == 'dev' - - watchfiles ; extra == 'dev' - editable: true - kind: pypi name: rerun-sdk version: 0.17.0 @@ -35509,7 +35481,7 @@ packages: requires_dist: - numpy - opencv-python>4.6 - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk - tqdm editable: true @@ -35521,7 +35493,7 @@ packages: requires_dist: - numpy - opencv-python>4.6 - - requests>=2.31,<3 + - requests<3,>=2.31 - rerun-sdk - tqdm editable: true @@ -36884,11 +36856,11 @@ packages: 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 + - segment-anything @ git+https://github.com/facebookresearch/segment-anything.git - torch==2.2.2 - torchvision - tqdm @@ -37119,7 +37091,7 @@ packages: requires_dist: - mesh-to-sdf @ git+https://github.com/marian42/mesh_to_sdf.git - numpy - - requests<3,>=2.31 + - requests>=2.31,<3 - rerun-sdk - scikit-learn>=1.1.3 - trimesh==3.15.2 @@ -37132,7 +37104,7 @@ packages: 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 @@ -37391,9 +37363,9 @@ packages: path: examples/python/structure_from_motion sha256: b20b79aa7bb2b4225b37d3cb28872a70dc7e9ab2ca9ab138b90d60fc8d7b4c15 requires_dist: - - numpy - opencv-python>4.6 - - requests<3,>=2.31 + - numpy + - requests>=2.31,<3 - rerun-sdk - tqdm editable: true @@ -37403,9 +37375,9 @@ packages: path: examples/python/structure_from_motion sha256: b20b79aa7bb2b4225b37d3cb28872a70dc7e9ab2ca9ab138b90d60fc8d7b4c15 requires_dist: - - opencv-python>4.6 - numpy - - requests>=2.31,<3 + - opencv-python>4.6 + - requests<3,>=2.31 - rerun-sdk - tqdm editable: true From d22f19ba6eb9ed5cf593a2a7e68764378de7e429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Fri, 15 Nov 2024 10:50:46 +0100 Subject: [PATCH 08/12] WIP: per frame almost working --- .../re_space_view_graph/src/layout/mod.rs | 16 +++++ .../re_space_view_graph/src/ui/state.rs | 68 +++++++++++++------ crates/viewer/re_space_view_graph/src/view.rs | 8 ++- crates/viewer/re_viewer/src/reflection/mod.rs | 2 + .../rust/node_link/src/examples/lattice.rs | 5 -- 5 files changed, 72 insertions(+), 27 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index 8a385a5da145..2261216423ca 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -81,6 +81,22 @@ impl ForceLayout { } } + pub fn init_layout(&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); diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 6bbecab1e621..4d5c653c7e7d 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -1,7 +1,6 @@ use egui::Rect; use re_chunk::{TimeInt, Timeline}; use re_format::format_f32; -use re_log::external::log; use re_types::blueprint::components::VisualBounds2D; use re_ui::UiExt; use re_viewer_context::SpaceViewState; @@ -90,8 +89,10 @@ pub enum LayoutState { impl LayoutState { pub fn bounding_rect(&self) -> Option { match self { - Self::Finished { layout, .. } => Some(layout.bounding_rect()), Self::None => None, + Self::Finished { layout, .. } | Self::InProgress { layout, .. } => { + Some(layout.bounding_rect()) + } } } @@ -99,32 +100,59 @@ impl LayoutState { 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>( + mut self, + requested: Timestamp, + graphs: impl Iterator> + Clone, + ) -> Self { + match self { + // Layout is up to date, nothing to do here. + Self::Finished { timestamp, layout } if timestamp == requested => { + Self::Finished { timestamp, layout } // no op + } + // We need to recompute the layout. + Self::None | Self::Finished { .. } => { + let provider = ForceLayout::new(graphs); + let layout = provider.init_layout(); + + Self::InProgress { + timestamp: requested, + layout, + provider, + } + } + // We keep iterating on the layout until it is stable. + Self::InProgress { + timestamp, + mut layout, + mut provider, + } => match provider.tick(&mut layout) { + true => Self::Finished { timestamp, layout }, + false => Self::InProgress { + timestamp, + layout, + provider, + }, + }, + } + } + /// This method is lazy. A new layout is only computed if the current timestamp requires it. - pub fn update<'a>( + pub fn get<'a>( &'a mut self, timeline: Timeline, time: TimeInt, graphs: impl Iterator> + Clone, ) -> &'a mut Layout { - let requested = Timestamp::new(timeline, time); - - match self { - Self::Finished { timestamp, .. } if timestamp == &requested => { - return match self { - Self::Finished { layout, .. } => layout, - _ => unreachable!(), // We just checked that the state is `Self::Current`. - }; - }, - Self::Finished { .. } => (), // TODO(grtlr): repurpose old layout - } - - *self = Self::Finished { - timestamp: requested, - layout, - }; + *self = std::mem::take(self).update(Timestamp::new(timeline, time), graphs); match self { - Self::Finished { layout, .. } => layout, + Self::Finished { layout, .. } | Self::InProgress { layout, .. } => layout, _ => 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 index 9f46fea3894c..dd578097bd75 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -146,10 +146,10 @@ impl SpaceViewClass for GraphSpaceView { bounds_property.component_or_fallback(ctx, self, state)?; let layout_was_empty = state.layout.is_none(); - let layout = state.layout.update( + let layout = state.layout.get( query.timeline, query.latest_at, - graphs.iter().map(|(ent, graph)| graph), + graphs.iter().map(|(_, graph)| graph), ); state.world_bounds = Some(bounds); @@ -218,6 +218,10 @@ impl SpaceViewClass for GraphSpaceView { // Update stored bounds on the state, so visualizers see an up-to-date value. state.world_bounds = Some(bounds); + if state.layout.is_in_progress() { + ui.ctx().request_repaint(); + } + Ok(()) } } diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index 854fc4953a2b..74276b83c771 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -1327,6 +1327,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ArchetypeName::new("rerun.archetypes.GraphEdges"), ArchetypeReflection { display_name: "Graph edges", + view_types: &["Graph View"], fields: vec![ ArchetypeFieldReflection { component_name : "rerun.components.GraphEdge".into(), display_name : "Edges", @@ -1343,6 +1344,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ArchetypeName::new("rerun.archetypes.GraphNodes"), ArchetypeReflection { display_name: "Graph nodes", + view_types: &["Graph View"], fields: vec![ ArchetypeFieldReflection { component_name : "rerun.components.GraphNode".into(), display_name : "Node ids", diff --git a/examples/rust/node_link/src/examples/lattice.rs b/examples/rust/node_link/src/examples/lattice.rs index e945975fa9ba..6130cb2c8f61 100644 --- a/examples/rust/node_link/src/examples/lattice.rs +++ b/examples/rust/node_link/src/examples/lattice.rs @@ -23,11 +23,6 @@ pub fn run(args: &Args, num_nodes: usize) -> anyhow::Result<()> { "/lattice", &GraphNodes::new(nodes) .with_colors(colors) - .with_positions(coordinates.clone().map(|(x, y)| { - let x_scaling = 100.0; - let y_scaling = 75.0; - [x as f32 * x_scaling, y as f32 * y_scaling] - })) .with_labels(coordinates.clone().map(|(x, y)| format!("({}, {})", x, y))), )?; From e6f16374fd17b40f7f976eddcb23ed445489c558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Fri, 15 Nov 2024 11:12:48 +0100 Subject: [PATCH 09/12] WIP: reset button --- crates/viewer/re_space_view_graph/src/layout/mod.rs | 12 ++++++++++-- crates/viewer/re_space_view_graph/src/ui/state.rs | 10 ++++++++++ crates/viewer/re_space_view_graph/src/view.rs | 7 +++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index 2261216423ca..f0fd2d658307 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -68,10 +68,18 @@ impl ForceLayout { .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)) - .add_force("charge", fj::ManyBody::new()) + .add_force( + "link", + fj::Link::new(all_edges) + .strength(1.0) + .distance(60.0) + .iterations(10), + ) + .add_force("charge", fj::ManyBody::new().strength(-30.0)) .add_force("x", fj::PositionX::new()) .add_force("y", fj::PositionY::new()); diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 4d5c653c7e7d..7418805ca950 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -43,6 +43,12 @@ impl GraphSpaceViewState { .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.reset(); + } + } } impl SpaceViewState for GraphSpaceViewState { @@ -96,6 +102,10 @@ impl LayoutState { } } + pub fn reset(&mut self) { + *self = Self::None; + } + pub fn is_none(&self) -> bool { matches!(self, Self::None) } diff --git a/crates/viewer/re_space_view_graph/src/view.rs b/crates/viewer/re_space_view_graph/src/view.rs index dd578097bd75..c67d702bc2b0 100644 --- a/crates/viewer/re_space_view_graph/src/view.rs +++ b/crates/viewer/re_space_view_graph/src/view.rs @@ -108,6 +108,7 @@ impl SpaceViewClass for GraphSpaceView { ui.selection_grid("graph_view_settings_ui").show(ui, |ui| { state.layout_ui(ui); + state.simulation_ui(ui); state.debug_ui(ui); }); @@ -168,7 +169,8 @@ impl SpaceViewClass for GraphSpaceView { for node in graph.nodes_explicit() { let pos = layout .get(&node.index) - .expect("explicit node should be in layout"); + .unwrap_or(egui::Rect::ZERO); // TODO(grtlr): sometimes there just isn't any data. + // .expect("explicit node should be in layout"); let response = scene.explicit_node(pos.min, node); layout.update(&node.index, response.rect); } @@ -177,7 +179,8 @@ impl SpaceViewClass for GraphSpaceView { for node in graph.nodes_implicit() { let current = layout .get(&node.index) - .expect("implicit node should be in layout"); + .unwrap_or(egui::Rect::ZERO); // TODO(grtlr): sometimes there just isn't any data. + // .expect("implicit node should be in layout"); let response = scene.implicit_node(current.min, node); layout.update(&node.index, response.rect); } From f8e9edad3f4ef44f6e2e1343eb821b6ecd371fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Fri, 15 Nov 2024 11:18:05 +0100 Subject: [PATCH 10/12] WIP: working for demo! --- crates/viewer/re_space_view_graph/src/layout/mod.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index f0fd2d658307..5b5cd36592db 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -72,16 +72,9 @@ impl ForceLayout { 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) - .strength(1.0) - .distance(60.0) - .iterations(10), - ) - .add_force("charge", fj::ManyBody::new().strength(-30.0)) - .add_force("x", fj::PositionX::new()) - .add_force("y", fj::PositionY::new()); + .add_force("link", fj::Link::new(all_edges)) + .add_force("charge", fj::ManyBody::new()) + .add_force("center", fj::Center::new()); Self { simulation, From 2e54352d8cefe192e33f1cf7f53cfe70e21d85dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Fri, 15 Nov 2024 15:42:11 +0100 Subject: [PATCH 11/12] WIP: small improvements --- crates/viewer/re_space_view_graph/src/layout/mod.rs | 2 ++ crates/viewer/re_space_view_graph/src/ui/state.rs | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/viewer/re_space_view_graph/src/layout/mod.rs b/crates/viewer/re_space_view_graph/src/layout/mod.rs index 5b5cd36592db..c3db0987e759 100644 --- a/crates/viewer/re_space_view_graph/src/layout/mod.rs +++ b/crates/viewer/re_space_view_graph/src/layout/mod.rs @@ -74,6 +74,8 @@ impl ForceLayout { .build(all_nodes) .add_force("link", fj::Link::new(all_edges)) .add_force("charge", fj::ManyBody::new()) + // TODO(grtlr): detect if we need a center force or a position force, depending on if the graph is disjoint + // or not. More generally: how much heuristics do we want to bake in here? .add_force("center", fj::Center::new()); Self { diff --git a/crates/viewer/re_space_view_graph/src/ui/state.rs b/crates/viewer/re_space_view_graph/src/ui/state.rs index 7418805ca950..5b42df7e005d 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -89,6 +89,7 @@ pub enum LayoutState { Finished { timestamp: Timestamp, layout: Layout, + _provider: ForceLayout, }, } @@ -122,8 +123,8 @@ impl LayoutState { ) -> Self { match self { // Layout is up to date, nothing to do here. - Self::Finished { timestamp, layout } if timestamp == requested => { - Self::Finished { timestamp, layout } // no op + Self::Finished { ref timestamp, .. } if timestamp == &requested => { + self // no op } // We need to recompute the layout. Self::None | Self::Finished { .. } => { @@ -142,7 +143,7 @@ impl LayoutState { mut layout, mut provider, } => match provider.tick(&mut layout) { - true => Self::Finished { timestamp, layout }, + true => Self::Finished { timestamp, layout, _provider: provider }, false => Self::InProgress { timestamp, layout, From 0ccef1277bacf43cf362f3d3cde0d64d9b40dfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Fri, 15 Nov 2024 16:27:00 +0100 Subject: [PATCH 12/12] WIP: create offset --- examples/rust/node_link/src/examples/binary_tree.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/rust/node_link/src/examples/binary_tree.rs b/examples/rust/node_link/src/examples/binary_tree.rs index c144c9744ad5..6ce9dd82b088 100644 --- a/examples/rust/node_link/src/examples/binary_tree.rs +++ b/examples/rust/node_link/src/examples/binary_tree.rs @@ -70,12 +70,6 @@ pub fn run(args: &Args) -> anyhow::Result<()> { ("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))), - // ("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(); @@ -111,6 +105,8 @@ pub fn run(args: &Args) -> anyhow::Result<()> { } } + let entity_offset_x = 400.0; + for level in levels_sorted { if !level.nodes.is_empty() { t += 1; @@ -119,7 +115,10 @@ pub fn run(args: &Args) -> anyhow::Result<()> { "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| nodes_sorted[n].1)), + .with_positions(level.nodes.iter().map(|n| { + let (x, y) = nodes_sorted[n].1; + [x + entity_offset_x, y] + })), ); }