use std::fmt::Write;
use rill_core::traits::processable::NodeVariant;
use rill_core::traits::{Node, NodeCategory};
use rill_core::Transcendental;
use crate::graph::Graph;
pub struct DotConfig {
pub cluster_by_category: bool,
pub show_port_names: bool,
pub show_parameters: bool,
pub rankdir: &'static str,
pub title: String,
}
impl Default for DotConfig {
fn default() -> Self {
Self {
cluster_by_category: true,
show_port_names: true,
show_parameters: false,
rankdir: "LR",
title: "Rill Signal Graph".into(),
}
}
}
pub fn to_dot<T: Transcendental, const B: usize>(
graph: &Graph<T, B>,
config: &DotConfig,
) -> String {
let mut dot = String::new();
writeln!(dot, "// {title}", title = config.title).ok();
writeln!(dot, "digraph rill_graph {{").ok();
writeln!(dot, " rankdir={rd};", rd = config.rankdir).ok();
writeln!(dot, " splines=true;").ok();
writeln!(dot, " node [style=filled, fontname=\"Sans\"];").ok();
writeln!(dot, " edge [fontname=\"Sans\", fontsize=10];").ok();
let nodes = graph.nodes();
if config.cluster_by_category {
emit_clusters(&mut dot, nodes, config);
} else {
for (idx, variant) in nodes.iter().enumerate() {
emit_node(&mut dot, idx, variant, config);
}
}
let topo = graph.topo_order();
for &node_idx in topo {
let variant = &nodes[node_idx];
let id = variant.id().inner();
for p in 0..variant.num_signal_outputs() {
if let Some(port) = variant.output_port(p) {
for &(target_node, _) in &port.downstream {
let target_entry = &nodes[target_node];
let target_id = target_entry.id().inner();
let from_label = port_name(&port_name_by_index(p, "out"));
write!(dot, " node_{id} -> node_{target_id}").ok();
if config.show_port_names {
write!(dot, " [label=\"sig {from_label}\"").ok();
} else {
write!(dot, " [label=\"\"").ok();
}
writeln!(dot, " color=\"#448\", fontcolor=\"#448\"];").ok();
}
}
}
}
writeln!(dot, "}}").ok();
dot
}
fn emit_node<T: Transcendental, const B: usize>(
dot: &mut String,
_idx: usize,
variant: &NodeVariant<T, B>,
config: &DotConfig,
) {
let meta = variant.metadata();
let id = variant.id().inner();
let cat = meta.category;
let (shape, fillcolor) = category_style(cat);
let mut label = String::new();
write!(label, "{}\\n", meta.name).ok();
if let Some(ref tn) = meta.type_name {
write!(label, "<i>{tn}</i>").ok();
}
if config.show_parameters {
for pm in meta.parameters.iter().take(5) {
if let Ok(pid) = rill_core::ParameterId::new(&pm.name) {
if let Some(val) = variant.get_parameter(&pid) {
let val_str = format!("{:?}", val);
write!(label, "\\n{}={}", pm.name, val_str).ok();
}
}
}
}
let escaped_label = label.replace('\"', "\\\"");
writeln!(dot, " node_{id} [label=\"{escaped_label}\"",).ok();
writeln!(dot, " shape={shape}").ok();
writeln!(dot, " fillcolor=\"{fillcolor}\"];").ok();
}
fn emit_clusters<T: Transcendental, const B: usize>(
dot: &mut String,
nodes: &[NodeVariant<T, B>],
config: &DotConfig,
) {
let mut by_cat: std::collections::BTreeMap<String, Vec<(usize, &NodeVariant<T, B>)>> =
std::collections::BTreeMap::new();
for (idx, variant) in nodes.iter().enumerate() {
let cat = variant.metadata().category;
let cat_name = cat.name();
let mut key = String::new();
let mut chars = cat_name.chars();
if let Some(c) = chars.next() {
key.extend(c.to_uppercase());
key.push_str(chars.as_str());
}
by_cat.entry(key).or_default().push((idx, variant));
}
for (cat_name, nodes) in &by_cat {
let first = nodes
.first()
.map(|(_, n)| n.metadata().category)
.unwrap_or(NodeCategory::Processor);
let (_, fillcolor) = category_style(first);
writeln!(dot, " subgraph cluster_{cat_name} {{").ok();
writeln!(dot, " label=\"{cat_name}\";").ok();
writeln!(dot, " style=filled;").ok();
writeln!(dot, " fillcolor=\"#f8f8f8\";").ok();
writeln!(dot, " fontcolor=\"{fillcolor}\";").ok();
writeln!(dot, " fontsize=14;").ok();
for (idx, variant) in nodes {
emit_node(dot, *idx, variant, config);
}
writeln!(dot, " }}").ok();
}
}
fn category_style(cat: NodeCategory) -> (&'static str, &'static str) {
match cat {
NodeCategory::Source => ("trapezium", "#8f8"),
NodeCategory::Processor => ("box", "#8bf"),
NodeCategory::Utility => ("diamond", "#fa8"),
NodeCategory::Sink => ("invtrapezium", "#f88"),
NodeCategory::Analyzer => ("note", "#ccc"),
NodeCategory::Sequencer => ("doublecircle", "#fcf"),
}
}
fn port_name(s: &str) -> String {
if s.is_empty() {
"?".to_string()
} else {
s.to_string()
}
}
fn port_name_by_index(idx: usize, dir: &str) -> String {
format!("{dir}_{idx}")
}