facett-flow/src/labels.rs
use egui::{FontId, Pos2, Rect, vec2};
use facett_core::legibility::{LabelCandidate, PlaceOpts, place_labels};
pub(crate) fn paint(
ui: &egui::Ui,
rect: Rect,
graph: &crate::Graph,
scene: &crate::scene::Scene,
scale: f32,
centre: Pos2,
) {
let world = Rect::from_min_max(
centre + (rect.min - rect.center()) / scale,
centre + (rect.max - rect.center()) / scale,
);
let indices = scene.visible(world, 2000);
let color = facett_core::theme(ui).text;
let galleys: Vec<_> = indices
.iter()
.map(|&i| {
ui.painter().layout_no_wrap(
graph.nodes[i].label.clone(),
FontId::proportional(11.0),
color,
)
})
.collect();
let candidates: Vec<_> = indices
.iter()
.zip(&galleys)
.map(|(&i, g)| {
LabelCandidate::new(
rect.center()
+ (Pos2::from(graph.nodes[i].position) - centre) * scale
+ vec2(0.0, 12.0),
g.size(),
0.0,
)
})
.collect();
for label in place_labels(&candidates, rect, PlaceOpts::default()).placed {
ui.painter().with_clip_rect(rect).galley(
label.rect.center() - galleys[label.index].size() * 0.5,
galleys[label.index].clone(),
color,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ten_thousand_node_label_pass_places_nonoverlapping_text() {
let graph = crate::Graph::demo(10_000);
let scene = crate::scene::Scene::new(&graph);
let rect = Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 700.0));
let scale =
(rect.width() / scene.bounds.width()).min(rect.height() / scene.bounds.height());
let ctx = egui::Context::default();
let mut output = ctx.run_ui(
egui::RawInput {
screen_rect: Some(rect),
..Default::default()
},
|ui| paint(ui, rect, &graph, &scene, scale, scene.bounds.center()),
);
let texts: Vec<_> = output
.shapes
.iter()
.filter_map(|s| match &s.shape {
egui::Shape::Text(t) => Some(
Rect::from_min_size(t.pos, t.galley.size())
.intersect(s.clip_rect)
.intersect(rect),
),
_ => None,
})
.filter(|r| r.is_positive())
.collect();
output.textures_delta.clear();
assert!(!texts.is_empty());
assert!(texts.len() <= 2000);
for (i, a) in texts.iter().enumerate() {
assert!(rect.intersects(*a));
for b in &texts[i + 1..] {
assert!(!a.intersects(*b), "placed labels overlap: {a:?}, {b:?}");
}
}
output.textures_delta.clear();
}
}