facett-flow/examples/flow_lab/scenarios.rs
//! Runtime fixture choices are separate from compiled features. A plain scene
//! remains plain even in an all-features binary.
#[cfg(feature = "host")]
use facett_core::Elm;
use facett_flow::{FlowGraph, Graph};
use serde_json::{Value, json};
pub const NAMES: &[&str] = &[
"plain",
"host",
"labels",
"boxes",
"arrows",
"status",
"rate",
"layered",
"compact-state",
"hit-test",
"lod",
"all",
];
pub struct Scenario {
pub name: String,
pub build_ms: f64,
#[cfg(feature = "host")]
pub scene: Option<facett_flow::FlowScene>,
}
impl Scenario {
pub fn validate(name: &str) -> Result<(), String> {
if !NAMES.contains(&name) {
return Err(format!("--scenario must be one of {}", NAMES.join(", ")));
}
for feature in if name == "all" {
&NAMES[1..NAMES.len() - 1]
} else {
std::slice::from_ref(&name)
} {
if *feature != "plain" && !facett_flow::compiled_features().contains(feature) {
return Err(format!("scenario {name} requires Cargo feature {feature}"));
}
}
Ok(())
}
pub fn new(name: &str, mut graph: Graph) -> Result<(FlowGraph, Self), String> {
Self::validate(name)?;
let start = std::time::Instant::now();
#[cfg(feature = "layered")]
if name == "layered" || name == "all" {
graph.layered(Default::default())?;
}
#[cfg(not(feature = "layered"))]
let _ = &mut graph;
let mut flow = FlowGraph::new("native-flow", graph)?;
#[cfg(feature = "labels")]
if name == "labels" {
flow.set_labels(true);
}
#[cfg(feature = "compact-state")]
if name == "compact-state" || name == "all" {
flow.set_compact_state(true);
}
#[cfg(not(any(feature = "labels", feature = "compact-state")))]
let _ = &mut flow;
#[cfg(feature = "host")]
let scene = if [
"host", "boxes", "arrows", "status", "rate", "hit-test", "lod", "all",
]
.contains(&name)
{
let mut scene =
facett_flow::FlowScene::with_key((*flow.state().graph).clone(), 0x464c_4142)?;
#[cfg(feature = "boxes")]
if name == "boxes" || name == "all" {
scene.set_sizes(facett_flow::attr::Sizes {
nodes: vec![[20.0, 12.0]; scene.graph().nodes.len()],
})?;
}
#[cfg(feature = "arrows")]
if name == "arrows" || name == "all" {
scene.set_arrows(true);
}
#[cfg(feature = "status")]
if name == "status" || name == "all" {
use facett_flow::attr::{Status, StatusTable};
let value = |i: usize| match i % 4 {
0 => None,
1 => Some(Status::Ok),
2 => Some(Status::Slow),
_ => Some(Status::Error),
};
scene.set_status(StatusTable {
nodes: (0..scene.graph().nodes.len()).map(value).collect(),
edges: (0..scene.graph().edges.len()).map(value).collect(),
})?;
}
#[cfg(feature = "rate")]
if name == "rate" || name == "all" {
scene.set_rates(facett_flow::attr::Rates {
edges: (0..scene.graph().edges.len())
.map(|i| match i % 4 {
0 => None,
1 => Some(0.0),
2 => Some(10.0),
_ => Some(100.0),
})
.collect(),
})?;
}
#[cfg(not(any(
feature = "boxes",
feature = "arrows",
feature = "status",
feature = "rate"
)))]
let _ = &mut scene;
Some(scene)
} else {
None
};
Ok((
flow,
Self {
name: name.into(),
build_ms: start.elapsed().as_secs_f64() * 1000.0,
#[cfg(feature = "host")]
scene,
},
))
}
pub fn metadata(&self) -> Value {
json!({"name":self.name,"scene_build_ms":self.build_ms,
"sizes_world":if self.name=="boxes" || self.name=="all"{Some([20,12])}else{None},
"label_budget":if self.name=="labels" || self.name=="all"{Some(2000)}else{None},
"rate_fixture":if self.name=="rate" || self.name=="all"{Some("repeating None,0,10,100")}else{None},
"status_fixture":if self.name=="status" || self.name=="all"{Some("repeating None,Ok,Slow,Error")}else{None},
"min_packet_px":if self.name=="lod" || self.name=="all"{12.0}else{0.0},
"hit_probe":self.name=="hit-test" || self.name=="all",
"compact_read_each_frame":self.name=="compact-state" || self.name=="all"})
}
pub fn bounds(&self, flow: &FlowGraph) -> egui::Rect {
#[cfg(feature = "host")]
if let Some(scene) = &self.scene {
return scene.bounds();
}
flow.bounds()
}
pub fn draw(&self, flow: &mut FlowGraph, ui: &mut egui::Ui) {
#[cfg(feature = "host")]
if let Some(scene) = &self.scene {
let (rect, _) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover());
let view = flow.state().camera.view(rect, scene.bounds());
let opts = facett_flow::PaintOpts {
seconds: flow.state().seconds,
moving: !flow.state().reduced_motion
&& facett_core::look::effects_policy(ui).allows_decorative_motion(),
clustered: flow.state().clustered,
cpu: facett_flow::CpuStyle::Full {
max_edges: usize::MAX,
},
#[cfg(feature = "lod")]
min_packet_px: if self.name == "lod" || self.name == "all" {
12.0
} else {
0.0
},
..Default::default()
};
let painted = facett_flow::paint(
ui,
egui::Id::new("flow-lab-host"),
rect,
scene,
&view,
&opts,
);
#[cfg(feature = "labels")]
if self.name == "all" && !flow.state().clustered {
use facett_core::legibility::{LabelCandidate, PlaceOpts, place_labels};
let col = facett_core::theme(ui).text;
let galleys: Vec<_> = painted
.on_screen
.iter()
.map(|n| {
ui.painter().layout_no_wrap(
scene.graph().nodes[n.node].label.clone(),
egui::FontId::proportional(11.0),
col,
)
})
.collect();
let candidates: Vec<_> = painted
.on_screen
.iter()
.zip(&galleys)
.map(|(n, g)| {
LabelCandidate::new(n.rect.center() + egui::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(),
col,
);
}
}
let _ = painted;
#[cfg(feature = "hit-test")]
if self.name == "hit-test" || self.name == "all" {
std::hint::black_box(
scene.hit_with_candidates(view.world(rect.center()), 4.0 / view.scale),
);
}
return;
}
facett_core::Facet::ui(flow, ui);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_scene_selection_is_explicit_and_validates_features() {
assert!(Scenario::validate("plain").is_ok());
assert!(Scenario::validate("unknown").is_err());
let (_, plain) = Scenario::new("plain", Graph::demo(20)).unwrap();
assert_eq!(plain.metadata()["name"], "plain");
#[cfg(feature = "host")]
assert!(plain.scene.is_none());
for name in NAMES.iter().filter(|n| **n != "plain") {
if Scenario::validate(name).is_ok() {
let (flow, scenario) = Scenario::new(name, Graph::demo(20)).unwrap();
assert_eq!(scenario.metadata()["name"], *name);
assert!(scenario.bounds(&flow).is_finite());
}
}
}
#[cfg(all(feature = "boxes", feature = "status", feature = "rate"))]
#[test]
fn attribute_scenarios_apply_real_parallel_tables() {
let (_, boxes) = Scenario::new("boxes", Graph::demo(20)).unwrap();
assert_eq!(
boxes.scene.unwrap().node_rect(0).unwrap().size(),
egui::vec2(20.0, 12.0)
);
let (_, status) = Scenario::new("status", Graph::demo(20)).unwrap();
assert!(status.scene.unwrap().status().unwrap().nodes[1].is_some());
let (_, rate) = Scenario::new("rate", Graph::demo(20)).unwrap();
assert_eq!(
&rate.scene.unwrap().rates().unwrap().edges[..4],
&[None, Some(0.0), Some(10.0), Some(100.0)]
);
}
}
#[cfg(all(
test,
feature = "host",
feature = "labels",
feature = "boxes",
feature = "arrows",
feature = "status",
feature = "rate",
feature = "layered",
feature = "compact-state",
feature = "hit-test",
feature = "lod"
))]
#[test]
#[ignore = "constructs the required 10k and 100k active-feature fixtures"]
fn combined_scene_accepts_required_benchmark_sizes() {
for count in [10_000, 100_000] {
let (flow, scenario) = Scenario::new("all", Graph::hub_demo(count)).unwrap();
assert_eq!(flow.state().graph.nodes.len(), count);
let scene = scenario.scene.as_ref().unwrap();
assert_eq!(scene.graph().nodes.len(), count);
assert_eq!(scene.status().unwrap().nodes.len(), count);
assert_eq!(scene.rates().unwrap().edges.len(), count - 1);
println!("{}", scenario.metadata());
}
}