use std::collections::HashMap;
use crate::validate::ValidatedConfig;
#[derive(Debug, Clone)]
pub struct PlacedNode {
pub alias: String,
pub layer: usize,
pub row: usize,
}
pub fn active_device_names(plan: &ValidatedConfig) -> std::collections::HashSet<String> {
let mut active = std::collections::HashSet::new();
for route in &plan.routes {
active.insert(route.from.clone());
active.insert(route.to.clone());
}
active
}
pub fn disconnected_device_names(plan: &ValidatedConfig) -> Vec<String> {
let active = active_device_names(plan);
plan.devices
.iter()
.map(|d| d.name.clone())
.filter(|name| !active.contains(name))
.collect()
}
pub fn cascade_hidden(
plan: &ValidatedConfig,
initial_hidden: &std::collections::HashSet<String>,
) -> std::collections::HashSet<String> {
let surviving_devices: std::collections::HashSet<String> = plan
.routes
.iter()
.filter(|r| !initial_hidden.contains(&r.from) && !initial_hidden.contains(&r.to))
.flat_map(|r| [r.from.clone(), r.to.clone()])
.collect();
let active = active_device_names(plan);
let mut hidden = initial_hidden.clone();
for name in &active {
if !surviving_devices.contains(name) {
hidden.insert(name.clone());
}
}
hidden
}
pub fn compute_layout(
plan: &ValidatedConfig,
exclude: &std::collections::HashSet<String>,
) -> Vec<PlacedNode> {
let active = active_device_names(plan);
let device_names: Vec<String> = plan
.devices
.iter()
.map(|d| d.name.clone())
.filter(|name| active.contains(name) && !exclude.contains(name))
.collect();
if device_names.is_empty() {
return Vec::new();
}
let mut successors: HashMap<&str, Vec<&str>> = HashMap::new();
let mut predecessors: HashMap<&str, Vec<&str>> = HashMap::new();
for route in &plan.routes {
if exclude.contains(&route.from) || exclude.contains(&route.to) {
continue;
}
{
let list = successors.entry(route.from.as_str()).or_default();
if !list.contains(&route.to.as_str()) {
list.push(route.to.as_str());
}
}
{
let list = predecessors.entry(route.to.as_str()).or_default();
if !list.contains(&route.from.as_str()) {
list.push(route.from.as_str());
}
}
}
let mut layer: HashMap<&str, usize> = HashMap::new();
for name in &device_names {
layer.insert(name.as_str(), 0);
}
let layer_cap = device_names.len();
let max_iters = device_names.len() + 1;
for _ in 0..max_iters {
let mut changed = false;
for name in &device_names {
if let Some(preds) = predecessors.get(name.as_str())
&& !preds.is_empty()
{
let max_pred = preds
.iter()
.map(|p| layer.get(p).copied().unwrap_or(0))
.max()
.unwrap_or(0);
let new_layer = (max_pred + 1).min(layer_cap);
if layer.get(name.as_str()).copied().unwrap_or(0) != new_layer {
layer.insert(name.as_str(), new_layer);
changed = true;
}
}
}
if !changed {
break;
}
}
let mut sorted_layers: Vec<usize> = layer.values().copied().collect();
sorted_layers.sort_unstable();
sorted_layers.dedup();
let remap: HashMap<usize, usize> = sorted_layers
.iter()
.enumerate()
.map(|(i, &l)| (l, i))
.collect();
for name in &device_names {
let l = layer[name.as_str()];
layer.insert(name.as_str(), remap[&l]);
}
let max_layer = layer.values().copied().max().unwrap_or(0);
let mut by_layer: Vec<Vec<&str>> = vec![Vec::new(); max_layer + 1];
for name in &device_names {
let l = layer.get(name.as_str()).copied().unwrap_or(0);
by_layer[l].push(name.as_str());
}
for l in 1..=max_layer {
let prev_pos: HashMap<&str, f32> = by_layer[l - 1]
.iter()
.enumerate()
.map(|(i, &n)| (n, i as f32))
.collect();
by_layer[l].sort_by(|a, b| {
let ba = barycenter(a, &predecessors, &prev_pos);
let bb = barycenter(b, &predecessors, &prev_pos);
ba.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
});
}
for l in (0..max_layer).rev() {
let next_pos: HashMap<&str, f32> = by_layer[l + 1]
.iter()
.enumerate()
.map(|(i, &n)| (n, i as f32))
.collect();
by_layer[l].sort_by(|a, b| {
let ba = barycenter(a, &successors, &next_pos);
let bb = barycenter(b, &successors, &next_pos);
ba.partial_cmp(&bb).unwrap_or(std::cmp::Ordering::Equal)
});
}
let mut result = Vec::new();
for (l, nodes) in by_layer.iter().enumerate() {
for (r, &name) in nodes.iter().enumerate() {
result.push(PlacedNode {
alias: name.to_string(),
layer: l,
row: r,
});
}
}
result
}
fn barycenter(
node: &str,
adjacency: &HashMap<&str, Vec<&str>>,
positions: &HashMap<&str, f32>,
) -> f32 {
match adjacency.get(node) {
Some(neighbors) if !neighbors.is_empty() => {
let relevant: Vec<f32> = neighbors
.iter()
.filter_map(|n| positions.get(n).copied())
.collect();
if relevant.is_empty() {
f32::MAX
} else {
relevant.iter().sum::<f32>() / relevant.len() as f32
}
}
_ => f32::MAX,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use std::collections::HashSet;
fn plan_from_toml(toml_str: &str) -> ValidatedConfig {
let config: Config = toml::from_str(toml_str).unwrap();
crate::validate::validate_config(config).unwrap()
}
fn layer_of(plan: &ValidatedConfig, alias: &str) -> usize {
compute_layout(plan, &Default::default())
.iter()
.find(|n| n.alias == alias)
.map(|n| n.layer)
.unwrap_or(usize::MAX)
}
const ENGINE: &str = "[engine]\nsample_rate = 48000\nbuffer_size = 256\n";
#[test]
fn linear_chain() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "b"
to = "c"
from_channels = [1]
to_channels = [1]
"#
));
let layout = compute_layout(&plan, &Default::default());
assert_eq!(layer_of(&plan, "a"), 0);
assert_eq!(layer_of(&plan, "b"), 1);
assert_eq!(layer_of(&plan, "c"), 2);
assert_eq!(layout.len(), 3);
}
#[test]
fn fan_out() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "a"
to = "c"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "a"
to = "d"
from_channels = [1]
to_channels = [1]
"#
));
assert_eq!(layer_of(&plan, "a"), 0);
assert_eq!(layer_of(&plan, "b"), 1);
assert_eq!(layer_of(&plan, "c"), 1);
assert_eq!(layer_of(&plan, "d"), 1);
let layout = compute_layout(&plan, &Default::default());
let layer1: Vec<&PlacedNode> = layout.iter().filter(|n| n.layer == 1).collect();
assert_eq!(layer1.len(), 3);
let rows: Vec<usize> = layer1.iter().map(|n| n.row).collect();
assert!(rows.contains(&0));
assert!(rows.contains(&1));
assert!(rows.contains(&2));
}
#[test]
fn fan_in() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "d"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "b"
to = "d"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "c"
to = "d"
from_channels = [1]
to_channels = [1]
"#
));
assert_eq!(layer_of(&plan, "a"), 0);
assert_eq!(layer_of(&plan, "b"), 0);
assert_eq!(layer_of(&plan, "c"), 0);
assert_eq!(layer_of(&plan, "d"), 1);
}
#[test]
fn diamond_graph() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "a"
to = "c"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "b"
to = "d"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "c"
to = "d"
from_channels = [1]
to_channels = [1]
"#
));
assert_eq!(layer_of(&plan, "a"), 0);
assert_eq!(layer_of(&plan, "b"), 1);
assert_eq!(layer_of(&plan, "c"), 1);
assert_eq!(layer_of(&plan, "d"), 2);
}
#[test]
fn intermediate_device() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "mixer"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "mixer"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let la = layer_of(&plan, "a");
let lm = layer_of(&plan, "mixer");
let lb = layer_of(&plan, "b");
assert!(la < lm);
assert!(lm < lb);
}
#[test]
fn long_chain_has_many_layers() {
let toml = format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "b"
to = "c"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "c"
to = "d"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "d"
to = "e"
from_channels = [1]
to_channels = [1]
"#
);
let plan = plan_from_toml(&toml);
let layout = compute_layout(&plan, &Default::default());
let max_layer = layout.iter().map(|n| n.layer).max().unwrap();
assert_eq!(max_layer, 4); }
#[test]
fn no_routes_empty_layout() {
let plan = ValidatedConfig {
config: toml::from_str(ENGINE).unwrap(),
devices: vec![],
routes: vec![],
warnings: vec![],
};
let layout = compute_layout(&plan, &Default::default());
assert!(layout.is_empty());
}
#[test]
fn barycenter_reduces_crossings() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a0"
to = "b1"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "a1"
to = "b0"
from_channels = [1]
to_channels = [1]
"#
));
let layout = compute_layout(&plan, &Default::default());
let b0 = layout.iter().find(|n| n.alias == "b0").unwrap();
let b1 = layout.iter().find(|n| n.alias == "b1").unwrap();
let a0 = layout.iter().find(|n| n.alias == "a0").unwrap();
let a1 = layout.iter().find(|n| n.alias == "a1").unwrap();
if a0.row < a1.row {
assert!(b0.row != b1.row);
}
}
#[test]
fn cycle_does_not_infinite_loop() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "b"
to = "a"
from_channels = [1]
to_channels = [1]
"#
));
let layout = compute_layout(&plan, &Default::default());
assert_eq!(layout.len(), 2);
assert!(layout.iter().all(|n| n.layer < usize::MAX));
}
#[test]
fn disconnected_devices_excluded_from_layout() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[devices]]
name = "lonely"
device = "LonelyDevice"
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let layout = compute_layout(&plan, &Default::default());
assert!(layout.iter().any(|n| n.alias == "a"));
assert!(layout.iter().any(|n| n.alias == "b"));
assert!(!layout.iter().any(|n| n.alias == "lonely"));
assert_eq!(layout.len(), 2);
}
#[test]
fn disconnected_device_names_correct() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[devices]]
name = "c"
device = "DevC"
[[devices]]
name = "d"
device = "DevD"
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let active = active_device_names(&plan);
assert!(active.contains("a"));
assert!(active.contains("b"));
assert!(!active.contains("c"));
assert!(!active.contains("d"));
let disconnected = disconnected_device_names(&plan);
assert_eq!(disconnected.len(), 2);
assert!(disconnected.contains(&"c".to_string()));
assert!(disconnected.contains(&"d".to_string()));
}
#[test]
fn all_devices_active_when_all_in_routes() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let disconnected = disconnected_device_names(&plan);
assert!(disconnected.is_empty());
}
#[test]
fn cascade_simple_chain_all_hidden() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let initial: HashSet<String> = ["a".to_string()].into_iter().collect();
let hidden = cascade_hidden(&plan, &initial);
assert!(hidden.contains("a"));
assert!(hidden.contains("b"));
}
#[test]
fn cascade_partial_survives() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "c"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let initial: HashSet<String> = ["a".to_string()].into_iter().collect();
let hidden = cascade_hidden(&plan, &initial);
assert!(hidden.contains("a"));
assert!(!hidden.contains("b"));
assert!(!hidden.contains("c"));
}
#[test]
fn cascade_no_hidden_returns_empty() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
"#
));
let initial: HashSet<String> = HashSet::new();
let hidden = cascade_hidden(&plan, &initial);
assert!(hidden.is_empty());
}
#[test]
fn cascade_multi_hop() {
let plan = plan_from_toml(&format!(
r#"
{ENGINE}
[[routes]]
from = "a"
to = "b"
from_channels = [1]
to_channels = [1]
[[routes]]
from = "b"
to = "c"
from_channels = [1]
to_channels = [1]
"#
));
let initial: HashSet<String> = ["a".to_string()].into_iter().collect();
let hidden = cascade_hidden(&plan, &initial);
assert!(hidden.contains("a"));
assert!(!hidden.contains("b"));
assert!(!hidden.contains("c"));
}
}