Skip to content

Commit 7107267

Browse files
authored
Clean up ForceLayoutProvider (#8339)
1 parent c2304a0 commit 7107267

File tree

4 files changed

+92
-89
lines changed

4 files changed

+92
-89
lines changed

crates/viewer/re_space_view_graph/src/layout/provider.rs

+59-78
Original file line numberDiff line numberDiff line change
@@ -27,115 +27,86 @@ impl<'a> From<&'a NodeTemplate> for fj::Node {
2727

2828
pub struct ForceLayoutProvider {
2929
simulation: fj::Simulation,
30-
node_index: ahash::HashMap<NodeId, usize>,
3130
pub request: LayoutRequest,
3231
}
3332

33+
fn considered_edges(request: &LayoutRequest) -> Vec<(usize, usize)> {
34+
let node_index: ahash::HashMap<NodeId, usize> = request
35+
.all_nodes()
36+
.enumerate()
37+
.map(|(i, (id, _))| (id, i))
38+
.collect();
39+
request
40+
.all_edges()
41+
.filter(|(id, _)| !id.is_self_edge())
42+
.map(|(id, _)| (node_index[&id.source], node_index[&id.target]))
43+
.collect()
44+
}
45+
3446
impl ForceLayoutProvider {
3547
pub fn new(request: LayoutRequest) -> Self {
36-
Self::new_impl(request, None)
37-
}
38-
39-
pub fn new_with_previous(request: LayoutRequest, layout: &Layout) -> Self {
40-
Self::new_impl(request, Some(layout))
41-
}
42-
43-
// TODO(grtlr): Consider consuming the old layout to avoid re-allocating the extents.
44-
// That logic has to be revised when adding the blueprints anyways.
45-
fn new_impl(request: LayoutRequest, layout: Option<&Layout>) -> Self {
46-
let nodes = request.graphs.iter().flat_map(|(_, graph_template)| {
47-
graph_template.nodes.iter().map(|n| {
48-
let mut fj_node = fj::Node::from(n.1);
49-
if let Some(rect) = layout.and_then(|l| l.get_node(n.0)) {
50-
let pos = rect.center();
51-
fj_node = fj_node.position(pos.x as f64, pos.y as f64);
52-
}
53-
54-
(n.0, fj_node)
55-
})
56-
});
57-
58-
let mut node_index = ahash::HashMap::default();
59-
let all_nodes: Vec<fj::Node> = nodes
60-
.enumerate()
61-
.map(|(i, n)| {
62-
node_index.insert(*n.0, i);
63-
n.1
64-
})
65-
.collect();
66-
67-
let all_edges_iter = request
68-
.graphs
69-
.iter()
70-
.flat_map(|(_, graph_template)| graph_template.edges.iter());
71-
72-
// Looking at self-edges does not make sense in a force-based layout, so we filter those out.
73-
let considered_edges = all_edges_iter
74-
.clone()
75-
.filter(|(id, _)| !id.is_self_edge())
76-
.map(|(id, _)| (node_index[&id.source], node_index[&id.target]));
48+
let nodes = request.all_nodes().map(|(_, v)| fj::Node::from(v));
49+
let edges = considered_edges(&request);
7750

7851
// TODO(grtlr): Currently we guesstimate good forces. Eventually these should be exposed as blueprints.
7952
let simulation = fj::SimulationBuilder::default()
8053
.with_alpha_decay(0.01) // TODO(grtlr): slows down the simulation for demo
81-
.build(all_nodes)
82-
.add_force(
83-
"link",
84-
fj::Link::new(considered_edges).distance(50.0).iterations(2),
85-
)
54+
.build(nodes)
55+
.add_force("link", fj::Link::new(edges).distance(50.0).iterations(2))
8656
.add_force("charge", fj::ManyBody::new())
8757
// TODO(grtlr): This is a small stop-gap until we have blueprints to prevent nodes from flying away.
8858
.add_force("x", fj::PositionX::new().strength(0.01))
8959
.add_force("y", fj::PositionY::new().strength(0.01));
9060

9161
Self {
9262
simulation,
93-
node_index,
9463
request,
9564
}
9665
}
9766

98-
pub fn init(&self) -> Layout {
99-
let positions = self.simulation.positions().collect::<Vec<_>>();
100-
let mut extents = ahash::HashMap::default();
101-
102-
for graph in self.request.graphs.values() {
103-
for (id, node) in &graph.nodes {
104-
let i = self.node_index[id];
105-
let [x, y] = positions[i];
106-
let pos = Pos2::new(x as f32, y as f32);
107-
extents.insert(*id, Rect::from_center_size(pos, node.size));
67+
pub fn new_with_previous(request: LayoutRequest, layout: &Layout) -> Self {
68+
let nodes = request.all_nodes().map(|(id, v)| {
69+
if let Some(rect) = layout.get_node(&id) {
70+
let pos = rect.center();
71+
fj::Node::from(v).position(pos.x as f64, pos.y as f64)
72+
} else {
73+
fj::Node::from(v)
10874
}
109-
}
75+
});
76+
let edges = considered_edges(&request);
77+
78+
// TODO(grtlr): Currently we guesstimate good forces. Eventually these should be exposed as blueprints.
79+
let simulation = fj::SimulationBuilder::default()
80+
.with_alpha_decay(0.01) // TODO(grtlr): slows down the simulation for demo
81+
.build(nodes)
82+
.add_force("link", fj::Link::new(edges).distance(50.0).iterations(2))
83+
.add_force("charge", fj::ManyBody::new())
84+
// TODO(grtlr): This is a small stop-gap until we have blueprints to prevent nodes from flying away.
85+
.add_force("x", fj::PositionX::new().strength(0.01))
86+
.add_force("y", fj::PositionY::new().strength(0.01));
11087

111-
Layout {
112-
nodes: extents,
113-
// Without any real node positions, we probably don't want to draw edges either.
114-
edges: ahash::HashMap::default(),
115-
entities: Vec::new(),
88+
Self {
89+
simulation,
90+
request,
11691
}
11792
}
11893

119-
/// Returns `true` if finished.
120-
pub fn tick(&mut self, layout: &mut Layout) -> bool {
121-
self.simulation.tick(1);
122-
123-
let positions = self.simulation.positions().collect::<Vec<_>>();
94+
fn layout(&self) -> Layout {
95+
// We make use of the fact here that the simulation is stable, i.e. the
96+
// order of the nodes is the same as in the `request`.
97+
let mut positions = self.simulation.positions();
12498

125-
// We clear all unnecessary data from the previous layout, but keep its space allocated.
126-
layout.entities.clear();
127-
layout.edges.clear();
99+
let mut layout = Layout::empty();
128100

129101
for (entity, graph) in &self.request.graphs {
130102
let mut current_rect = Rect::NOTHING;
131103

132-
for node in graph.nodes.keys() {
133-
let extent = layout.nodes.get_mut(node).expect("node has to be present");
134-
let i = self.node_index[node];
135-
let [x, y] = positions[i];
104+
for (node, template) in &graph.nodes {
105+
let [x, y] = positions.next().expect("positions has to match the layout");
136106
let pos = Pos2::new(x as f32, y as f32);
137-
extent.set_center(pos);
138-
current_rect = current_rect.union(*extent);
107+
let extent = Rect::from_center_size(pos, template.size);
108+
current_rect = current_rect.union(extent);
109+
layout.nodes.insert(*node, extent);
139110
}
140111

141112
layout.entities.push((entity.clone(), current_rect));
@@ -248,6 +219,16 @@ impl ForceLayoutProvider {
248219
}
249220
}
250221

222+
layout
223+
}
224+
225+
/// Returns `true` if finished.
226+
pub fn tick(&mut self) -> Layout {
227+
self.simulation.tick(1);
228+
self.layout()
229+
}
230+
231+
pub fn is_finished(&self) -> bool {
251232
self.simulation.finished()
252233
}
253234
}

crates/viewer/re_space_view_graph/src/layout/request.rs

+14
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,18 @@ impl LayoutRequest {
7878

7979
request
8080
}
81+
82+
/// Returns all nodes from all graphs in this request.
83+
pub(super) fn all_nodes(&self) -> impl Iterator<Item = (NodeId, &NodeTemplate)> + '_ {
84+
self.graphs
85+
.iter()
86+
.flat_map(|(_, graph)| graph.nodes.iter().map(|(k, v)| (*k, v)))
87+
}
88+
89+
/// Returns all edges from all graphs in this request.
90+
pub(super) fn all_edges(&self) -> impl Iterator<Item = (EdgeId, &[EdgeTemplate])> + '_ {
91+
self.graphs
92+
.iter()
93+
.flat_map(|(_, graph)| graph.edges.iter().map(|(k, v)| (*k, v.as_slice())))
94+
}
8195
}

crates/viewer/re_space_view_graph/src/layout/result.rs

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ fn bounding_rect_from_iter(rectangles: impl Iterator<Item = egui::Rect>) -> egui
1919
}
2020

2121
impl Layout {
22+
/// Creates an empty layout
23+
pub fn empty() -> Self {
24+
Self {
25+
nodes: ahash::HashMap::default(),
26+
edges: ahash::HashMap::default(),
27+
entities: Vec::new(),
28+
}
29+
}
30+
2231
/// Returns the bounding rectangle of the layout.
2332
pub fn bounding_rect(&self) -> Rect {
2433
// TODO(grtlr): We mostly use this for debugging, but we should probably

crates/viewer/re_space_view_graph/src/ui/state.rs

+10-11
Original file line numberDiff line numberDiff line change
@@ -105,34 +105,33 @@ impl LayoutState {
105105
}
106106
// We need to recompute the layout.
107107
Self::None => {
108-
let provider = ForceLayoutProvider::new(new_request);
109-
let layout = provider.init();
110-
108+
let mut provider = ForceLayoutProvider::new(new_request);
109+
let layout = provider.tick();
111110
Self::InProgress { layout, provider }
112111
}
113112
Self::Finished { layout, .. } => {
114113
let mut provider = ForceLayoutProvider::new_with_previous(new_request, &layout);
115-
let mut layout = provider.init();
116-
provider.tick(&mut layout);
117-
114+
let layout = provider.tick();
118115
Self::InProgress { layout, provider }
119116
}
120117
Self::InProgress {
121118
layout, provider, ..
122119
} if provider.request != new_request => {
123120
let mut provider = ForceLayoutProvider::new_with_previous(new_request, &layout);
124-
let mut layout = provider.init();
125-
provider.tick(&mut layout);
121+
let layout = provider.tick();
126122

127123
Self::InProgress { layout, provider }
128124
}
129125
// We keep iterating on the layout until it is stable.
130126
Self::InProgress {
131-
mut layout,
132127
mut provider,
133-
} => match provider.tick(&mut layout) {
128+
layout,
129+
} => match provider.is_finished() {
134130
true => Self::Finished { layout, provider },
135-
false => Self::InProgress { layout, provider },
131+
false => Self::InProgress {
132+
layout: provider.tick(),
133+
provider,
134+
},
136135
},
137136
}
138137
}

0 commit comments

Comments
 (0)