use super::{GROUP_HEAD, GROUP_PAD, measure::Model, model::Diagram};
fn frame_range(
diagram: &Diagram,
names: &[f64],
cross: &[f64],
cross_size: &impl Fn(usize) -> f64,
group: usize,
landscape: bool,
) -> (f64, f64) {
let own = (0..diagram.nodes.len())
.filter(|node| diagram.nodes[*node].group == Some(group))
.map(|node| {
(
cross[node] - cross_size(node) / 2.0,
cross[node] + cross_size(node) / 2.0,
)
});
let inner = (0..diagram.groups.len())
.filter(|child| diagram.groups[*child].parent == Some(group))
.map(|child| frame_range(diagram, names, cross, cross_size, child, landscape));
let (low, high) = own
.chain(inner)
.fold((f64::INFINITY, f64::NEG_INFINITY), |(low, high), (a, b)| {
(low.min(a), high.max(b))
});
let head = if landscape { GROUP_HEAD } else { 0.0 };
let extra = if landscape {
0.0
} else {
(names[group] - (high - low + 2.0 * GROUP_PAD)).max(0.0) / 2.0
};
(low - GROUP_PAD - head - extra, high + GROUP_PAD + extra)
}
#[allow(clippy::cast_precision_loss)]
pub(super) fn clear_frames(
model: &Model,
cross: &mut [f64],
cross_size: &impl Fn(usize) -> f64,
landscape: bool,
) {
const MARGIN: f64 = 12.0;
let (graph, diagram) = (&model.graph, model.diagram);
let before = cross.to_vec();
let mut order: Vec<usize> = (0..diagram.groups.len()).collect();
crate::sort::by_key(&mut order, |group| {
(std::cmp::Reverse(diagram.chain(Some(*group)).len()), *group)
});
let item_size = |item: usize| cross_size(item);
for &group in &order {
let layers: Vec<usize> = (0..diagram.nodes.len())
.filter(|node| diagram.inside(*node, group))
.map(|node| graph.layer[node])
.collect();
let first = layers.iter().copied().min().unwrap_or(0);
let last = layers.iter().copied().max().unwrap_or(0);
for layer in first..=last {
let items = &graph.layers[layer];
for position in 0..items.len() {
let item = items[position];
let Some(node) = graph.items[item].node else {
continue;
};
if diagram.inside(node, group) {
continue;
}
let (low, high) = frame_range(
diagram,
&model.group_names,
cross,
cross_size,
group,
landscape,
);
let half = item_size(item) / 2.0;
let (start, end) = (cross[item] - half - MARGIN, cross[item] + half + MARGIN);
if end <= low || start >= high {
continue;
}
let member = |other: &usize| {
graph.items[*other]
.node
.is_some_and(|node| diagram.inside(node, group))
};
let before = items[..position].iter().any(member);
let after = items[position + 1..].iter().any(member);
let left = if before == after {
cross[item] < f64::midpoint(low, high)
} else {
after
};
if left {
let shift = low - end;
for other in &items[..=position] {
cross[*other] += shift;
}
} else {
let shift = high - start;
for other in &items[position..] {
cross[*other] += shift;
}
}
}
}
}
separate_frames(model, cross, cross_size, landscape);
let budget: f64 = (0..graph.items.len()).map(cross_size).sum::<f64>()
+ diagram.groups.len() as f64 * 2.0 * (GROUP_PAD + GROUP_HEAD + MARGIN);
let reach = |cross: &[f64]| {
let (low, high) = frames_reach(model, cross, cross_size, landscape);
(0..graph.items.len())
.map(|item| cross[item] + cross_size(item) / 2.0)
.fold(high, f64::max)
- (0..graph.items.len())
.map(|item| cross[item] - cross_size(item) / 2.0)
.fold(low, f64::min)
};
if reach(cross) > budget {
cross.copy_from_slice(&before);
}
let low = (0..graph.items.len())
.map(|item| cross[item] - cross_size(item) / 2.0)
.fold(f64::INFINITY, f64::min)
.min(frames_reach(model, cross, cross_size, landscape).0);
for position in cross.iter_mut() {
*position -= low;
}
}
fn separate_frames(
model: &Model,
cross: &mut [f64],
cross_size: &impl Fn(usize) -> f64,
landscape: bool,
) {
const SEPARATION: f64 = 16.0;
let (graph, diagram) = (&model.graph, model.diagram);
let groups = diagram.groups.len();
let span = |group: usize| -> Option<(usize, usize)> {
let layers = (0..diagram.nodes.len())
.filter(|node| diagram.inside(*node, group))
.map(|node| graph.layer[node]);
layers.clone().min().zip(layers.max())
};
let related = |a: usize, b: usize| {
diagram.chain(Some(a)).contains(&b) || diagram.chain(Some(b)).contains(&a)
};
for _ in 0..=groups * groups {
let mut moved = false;
for a in 0..groups {
for b in 0..groups {
if a == b || related(a, b) {
continue;
}
let (Some((a_first, a_last)), Some((b_first, b_last))) = (span(a), span(b)) else {
continue;
};
if a_last < b_first || b_last < a_first {
continue;
}
let (a_low, a_high) =
frame_range(diagram, &model.group_names, cross, cross_size, a, landscape);
let (b_low, b_high) =
frame_range(diagram, &model.group_names, cross, cross_size, b, landscape);
if a_low + a_high >= b_low + b_high
|| a_high + SEPARATION <= b_low
|| b_high <= a_low
{
continue;
}
let shift = a_high + SEPARATION - b_low;
let anchor = (0..diagram.nodes.len())
.filter(|node| diagram.inside(*node, b))
.map(|node| cross[node])
.fold(f64::INFINITY, f64::min);
for layer in b_first..=b_last {
for &item in &graph.layers[layer] {
if cross[item] >= anchor {
cross[item] += shift;
}
}
}
moved = true;
}
}
if !moved {
break;
}
}
}
pub(super) fn frames_reach(
model: &Model,
cross: &[f64],
cross_size: &impl Fn(usize) -> f64,
landscape: bool,
) -> (f64, f64) {
let diagram = model.diagram;
(0..diagram.groups.len())
.filter(|group| diagram.groups[*group].parent.is_none())
.map(|group| {
frame_range(
diagram,
&model.group_names,
cross,
cross_size,
group,
landscape,
)
})
.fold((f64::INFINITY, f64::NEG_INFINITY), |(low, high), (a, b)| {
(low.min(a), high.max(b))
})
}