pub mod settings;
use bevy_utils::intern::Interned;
pub use settings::Settings;
use self::iter_utils::sorted;
use crate::dot::{font_tag, html_escape, DotGraph};
use bevy_render::render_graph::{Edge, RenderGraph, RenderLabel};
use iter_utils::EitherOrBoth;
use pretty_type_name::pretty_type_name_str;
use std::collections::HashMap;
pub fn render_graph_dot(graph: &RenderGraph, settings: &Settings) -> String {
let mut dot = DotGraph::digraph("RenderGraph", &[("rankdir", "LR"), ("ranksep", "1.0")])
.graph_attributes(&[("bgcolor", &settings.style.color_background)])
.edge_attributes(&[
("fontname", &settings.style.fontname),
("fontcolor", &settings.style.color_text),
])
.node_attributes(&[
("shape", "plaintext"),
("fontname", &settings.style.fontname),
("fontcolor", &settings.style.color_text),
]);
build_dot_graph(&mut dot, None, graph, settings, 0);
dot.finish()
}
fn build_dot_graph(
dot: &mut DotGraph,
graph_name: Option<&str>,
graph: &RenderGraph,
settings: &Settings,
subgraph_nest_level: usize,
) {
let fmt_label = |label: Interned<dyn RenderLabel>| format!("{:?}", label);
let node_mapping: HashMap<_, _> = graph
.iter_nodes()
.map(|node| {
(
node.label,
format!("{}{:?}", graph_name.unwrap_or_default(), node.label),
)
})
.collect();
let node_id = |id: &Interned<dyn RenderLabel>| {
format!("{}_{}", graph_name.unwrap_or_default(), &node_mapping[id])
};
let mut nodes: Vec<_> = graph.iter_nodes().collect();
nodes.sort_by(|a, b| {
a.type_name
.cmp(b.type_name)
.then_with(|| fmt_label(a.label).cmp(&fmt_label(b.label)))
});
let layer_style = &settings.style.layers[subgraph_nest_level % settings.style.layers.len()];
let next_layer_style =
&settings.style.layers[(subgraph_nest_level + 1) % settings.style.layers.len()];
for (name, subgraph) in sorted(graph.iter_sub_graphs(), |(name, _)| format!("{:?}", name)) {
let internal_name = format!("{}_{:?}", graph_name.unwrap_or_default(), name);
let mut sub_dot = DotGraph::subgraph(
&internal_name,
&[("label", &format!("{:?}", name)), ("fontcolor", "red")],
)
.graph_attributes(&[
("style", "rounded,filled"),
("color", &next_layer_style.color_background),
("fontcolor", &next_layer_style.color_label),
]);
build_dot_graph(
&mut sub_dot,
Some(&internal_name),
subgraph,
settings,
subgraph_nest_level + 1,
);
dot.add_sub_graph(sub_dot);
}
for node in &nodes {
let name = &fmt_label(node.label);
let type_name = pretty_type_name_str(node.type_name);
let inputs = node
.input_slots
.iter()
.enumerate()
.map(|(index, slot)| {
format!(
"<TD PORT=\"{}\">{}: {}</TD>",
html_escape(&format!("in-{}", index)),
html_escape(&slot.name),
html_escape(&format!("{:?}", slot.slot_type))
)
})
.collect::<Vec<_>>();
let outputs = node
.output_slots
.iter()
.enumerate()
.map(|(index, slot)| {
format!(
"<TD PORT=\"{}\">{}: {}</TD>",
html_escape(&format!("out-{}", index)),
html_escape(&slot.name),
html_escape(&format!("{:?}", slot.slot_type))
)
})
.collect::<Vec<_>>();
let slots = iter_utils::zip_longest(inputs.iter(), outputs.iter())
.map(|pair| match pair {
EitherOrBoth::Both(input, output) => format!("<TR>{}{}</TR>", input, output),
EitherOrBoth::Left(input) => {
format!("<TR>{}<TD BORDER=\"0\"> </TD></TR>", input)
}
EitherOrBoth::Right(output) => {
format!("<TR><TD BORDER=\"0\"> </TD>{}</TR>", output)
}
})
.collect::<String>();
let label = format!(
"<<TABLE STYLE=\"rounded\"><TR><TD PORT=\"title\" BORDER=\"0\" COLSPAN=\"2\">{}<BR/>{}</TD></TR>{}</TABLE>>",
html_escape(name),
font_tag(&type_name, &settings.style.color_typename, 10),
slots,
);
dot.add_node(
&node_id(&node.label),
&[
("label", &label),
("color", &settings.style.color_node),
("fillcolor", &settings.style.color_node),
],
);
}
for node in &nodes {
for edge in node.edges.input_edges() {
match edge {
Edge::SlotEdge {
input_node,
input_index,
output_node,
output_index,
} => {
dot.add_edge_with_ports(
&node_id(output_node),
Some(&format!("out-{}:e", output_index)),
&node_id(input_node),
Some(&format!("in-{}:w", input_index)),
&[("color", &layer_style.color_edge_slot)],
);
}
Edge::NodeEdge {
input_node,
output_node,
} => {
dot.add_edge_with_ports(
&node_id(output_node),
Some("title:e"),
&node_id(input_node),
Some("title:w"),
&[("color", &layer_style.color_edge)],
);
}
}
}
}
}
mod iter_utils {
use std::iter::Fuse;
pub fn sorted<'a, T: 'a, U: Ord>(
iter: impl IntoIterator<Item = T>,
key: impl Fn(&T) -> U,
) -> impl IntoIterator<Item = T> + 'a {
let mut vec: Vec<_> = iter.into_iter().collect();
vec.sort_by_key(key);
vec.into_iter()
}
pub enum EitherOrBoth<A, B> {
Both(A, B),
Left(A),
Right(B),
}
#[derive(Clone, Debug)]
pub struct ZipLongest<T, U> {
a: Fuse<T>,
b: Fuse<U>,
}
pub fn zip_longest<T, U>(a: T, b: U) -> ZipLongest<T, U>
where
T: Iterator,
U: Iterator,
{
ZipLongest {
a: a.fuse(),
b: b.fuse(),
}
}
impl<T, U> Iterator for ZipLongest<T, U>
where
T: Iterator,
U: Iterator,
{
type Item = EitherOrBoth<T::Item, U::Item>;
fn next(&mut self) -> Option<Self::Item> {
match (self.a.next(), self.b.next()) {
(None, None) => None,
(Some(a), None) => Some(EitherOrBoth::Left(a)),
(None, Some(b)) => Some(EitherOrBoth::Right(b)),
(Some(a), Some(b)) => Some(EitherOrBoth::Both(a, b)),
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
max_size_hint(self.a.size_hint(), self.b.size_hint())
}
}
fn max_size_hint(
a: (usize, Option<usize>),
b: (usize, Option<usize>),
) -> (usize, Option<usize>) {
let (a_lower, a_upper) = a;
let (b_lower, b_upper) = b;
let lower = std::cmp::max(a_lower, b_lower);
let upper = match (a_upper, b_upper) {
(Some(x), Some(y)) => Some(std::cmp::max(x, y)),
_ => None,
};
(lower, upper)
}
}