diff --git a/crates/viewer/re_space_view_graph/src/layout/provider.rs b/crates/viewer/re_space_view_graph/src/layout/provider.rs index e6482ea1be1d..1036a2b20797 100644 --- a/crates/viewer/re_space_view_graph/src/layout/provider.rs +++ b/crates/viewer/re_space_view_graph/src/layout/provider.rs @@ -33,11 +33,26 @@ pub struct ForceLayoutProvider { impl ForceLayoutProvider { pub fn new(request: LayoutRequest) -> Self { + Self::new_impl(request, None) + } + + pub fn new_with_previous(request: LayoutRequest, layout: &Layout) -> Self { + Self::new_impl(request, Some(layout)) + } + + // TODO(grtlr): Consider consuming the old layout to avoid re-allocating the extents. + // That logic has to be revised when adding the blueprints anyways. + fn new_impl(request: LayoutRequest, layout: Option<&Layout>) -> Self { let nodes = request.graphs.iter().flat_map(|(_, graph_template)| { - graph_template - .nodes - .iter() - .map(|n| (n.0, fj::Node::from(n.1))) + graph_template.nodes.iter().map(|n| { + let mut fj_node = fj::Node::from(n.1); + if let Some(rect) = layout.and_then(|l| l.get_node(n.0)) { + let pos = rect.center(); + fj_node = fj_node.position(pos.x as f64, pos.y as f64); + } + + (n.0, fj_node) + }) }); let mut node_index = ahash::HashMap::default(); 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 dad235b3586a..749e3720f937 100644 --- a/crates/viewer/re_space_view_graph/src/ui/state.rs +++ b/crates/viewer/re_space_view_graph/src/ui/state.rs @@ -104,15 +104,25 @@ impl LayoutState { self // no op } // We need to recompute the layout. - Self::None | Self::Finished { .. } => { + Self::None => { let provider = ForceLayoutProvider::new(new_request); let layout = provider.init(); Self::InProgress { layout, provider } } - Self::InProgress { provider, .. } if provider.request != new_request => { - let provider = ForceLayoutProvider::new(new_request); - let layout = provider.init(); + Self::Finished { layout, .. } => { + let mut provider = ForceLayoutProvider::new_with_previous(new_request, &layout); + let mut layout = provider.init(); + provider.tick(&mut layout); + + Self::InProgress { layout, provider } + } + Self::InProgress { + layout, provider, .. + } if provider.request != new_request => { + let mut provider = ForceLayoutProvider::new_with_previous(new_request, &layout); + let mut layout = provider.init(); + provider.tick(&mut layout); Self::InProgress { layout, provider } } diff --git a/tests/python/release_checklist/check_graph_time_layout.py b/tests/python/release_checklist/check_graph_time_layout.py new file mode 100644 index 000000000000..3d469e1b72c5 --- /dev/null +++ b/tests/python/release_checklist/check_graph_time_layout.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import os +import random +from argparse import Namespace +from uuid import uuid4 + +import rerun as rr +import rerun.blueprint as rrb + +README = """\ +# Time-varying graph view + +Please watch out for any twitching, jumping, or other wise unexpected changes to +the layout when navigating the timeline. + +Please check the following: +* Scrub the timeline to see how the graph layout changes over time. +""" + + +def log_readme() -> None: + rr.log("readme", rr.TextDocument(README, media_type=rr.MediaType.MARKDOWN), static=True) + + +def log_graphs() -> None: + nodes = ["root"] + edges = [] + + # Randomly add nodes and edges to the graph + for i in range(50): + existing = random.choice(nodes) + new_node = str(i) + nodes.append(new_node) + edges.append((existing, new_node)) + + rr.set_time_sequence("frame", i) + rr.log("graph", rr.GraphNodes(nodes, labels=nodes), rr.GraphEdges(edges, graph_type=rr.GraphType.Directed)) + + rr.send_blueprint( + rrb.Blueprint( + rrb.Grid( + rrb.GraphView(origin="graph", name="Graph"), + rrb.TextDocumentView(origin="readme", name="Instructions"), + ) + ) + ) + + +def run(args: Namespace) -> None: + rr.script_setup(args, f"{os.path.basename(__file__)}", recording_id=uuid4()) + + log_readme() + log_graphs() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Interactive release checklist") + rr.script_add_args(parser) + args = parser.parse_args() + run(args)