use std::collections::BTreeMap;
use unicode_width::UnicodeWidthStr;
use super::flow::{NodeSizes, Placed};
use super::ordering::assign_positions;
use super::{Edge, Graph};
use crate::tui::terminal_graph::painter::{GAP_X, GAP_Y, MAX_LABEL};
#[derive(Clone, Copy)]
enum CrossAxisAlignment {
Exact,
Near,
}
#[derive(Clone, Copy)]
enum LaneAxis {
Vertical,
Horizontal,
}
impl CrossAxisAlignment {
fn has_jog(self, from: usize, to: usize) -> bool {
match self {
Self::Exact => from != to,
Self::Near => !near_aligned(from, to),
}
}
}
fn near_aligned(from: usize, to: usize) -> bool {
from.abs_diff(to) <= 1
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum EdgeRoute {
SelfLoop,
Adjacent,
Skip,
Back,
}
pub(super) fn edge_route(edge: &Edge, ranks: &[usize]) -> EdgeRoute {
if edge.from == edge.to {
EdgeRoute::SelfLoop
} else if ranks[edge.to] == ranks[edge.from] + 1 {
EdgeRoute::Adjacent
} else if ranks[edge.to] > ranks[edge.from] + 1 {
EdgeRoute::Skip
} else {
EdgeRoute::Back
}
}
struct BusSpan {
start: usize,
end: usize,
from: usize,
to: usize,
index: usize,
jogs: bool,
}
fn bus_spans(
graph: &Graph,
ranks: &[usize],
centers: &[usize],
r: usize,
alignment: CrossAxisAlignment,
) -> Vec<BusSpan> {
graph
.edges
.iter()
.enumerate()
.filter(|(_, e)| edge_route(e, ranks) == EdgeRoute::Adjacent && ranks[e.from] == r)
.map(|(i, e)| BusSpan {
start: centers[e.from].min(centers[e.to]),
end: centers[e.from].max(centers[e.to]),
from: e.from,
to: e.to,
index: i,
jogs: alignment.has_jog(centers[e.from], centers[e.to]),
})
.collect()
}
struct SkipEdge {
index: usize,
from: usize,
to: usize,
source_center: usize,
target_center: usize,
source_rank: usize,
target_rank: usize,
}
fn skip_edges(graph: &Graph, ranks: &[usize], centers: &[usize]) -> Vec<SkipEdge> {
graph
.edges
.iter()
.enumerate()
.filter(|(_, e)| edge_route(e, ranks) == EdgeRoute::Skip)
.map(|(index, e)| SkipEdge {
index,
from: e.from,
to: e.to,
source_center: centers[e.from],
target_center: centers[e.to],
source_rank: ranks[e.from],
target_rank: ranks[e.to],
})
.collect()
}
fn td_source_anchors(graph: &Graph, ranks: &[usize], centers: &[usize]) -> Vec<usize> {
let mut source_anchors = centers.to_vec();
for edge in &graph.edges {
let source_x = centers[edge.from];
let target_x = centers[edge.to];
if edge_route(edge, ranks) == EdgeRoute::Adjacent && near_aligned(source_x, target_x) {
source_anchors[edge.from] = target_x;
}
}
source_anchors
}
struct LaneSpan {
start: usize,
end: usize,
from: usize,
to: usize,
index: usize,
}
fn lane_spans(graph: &Graph, ranks: &[usize], placed: &[Placed], axis: LaneAxis) -> Vec<LaneSpan> {
graph
.edges
.iter()
.enumerate()
.filter(|(_, e)| matches!(edge_route(e, ranks), EdgeRoute::Skip | EdgeRoute::Back))
.map(|(index, e)| {
let (pf, pt) = (&placed[e.from], &placed[e.to]);
let (start, end) = match axis {
LaneAxis::Vertical => (pf.cy.min(pt.cy), pf.cy.max(pt.cy)),
LaneAxis::Horizontal => (pf.cx.min(pt.cx), pf.cx.max(pt.cx)),
};
LaneSpan {
start,
end,
from: e.from,
to: e.to,
index,
}
})
.collect()
}
fn label_block_width(lines: &[String]) -> usize {
lines.iter().map(|line| line.width()).max().unwrap_or(0)
}
fn extra_label_rows(lines: &[String]) -> usize {
lines.len().saturating_sub(1)
}
fn max_extra_label_rows(
graph: &Graph,
edge_labels: &[Vec<String>],
mut include: impl FnMut(&Edge) -> bool,
) -> usize {
graph
.edges
.iter()
.enumerate()
.filter(|(_, edge)| include(edge))
.map(|(index, _)| extra_label_rows(&edge_labels[index]))
.max()
.unwrap_or(0)
}
fn label_gap_rows(
graph: &Graph,
ranks: &[usize],
edge_labels: &[Vec<String>],
max_rank: usize,
) -> Vec<usize> {
let mut rows = vec![0usize; max_rank + 1];
for (index, edge) in graph.edges.iter().enumerate() {
if matches!(
edge_route(edge, ranks),
EdgeRoute::Adjacent | EdgeRoute::Skip
) {
let extra = extra_label_rows(&edge_labels[index]);
if extra > 0 {
let gap = ranks[edge.to] - 1;
rows[gap] = rows[gap].max(extra);
}
}
}
rows
}
pub(super) fn place_td(
ranks: &[usize],
max_rank: usize,
by_rank: &[Vec<usize>],
sizes: &NodeSizes,
graph: &Graph,
edge_labels: &[Vec<String>],
placed: &mut [Placed],
) -> RoutePlan {
let centers = assign_positions(by_rank, &sizes.lay_w, GAP_X, &graph.edges, ranks);
let mut source_anchors = td_source_anchors(graph, ranks, ¢ers);
let skips = skip_edges(graph, ranks, ¢ers);
let mut edge_bus = vec![0usize; graph.edges.len()];
let mut join_slots = vec![0usize; graph.edges.len()];
let mut bus_tracks = vec![0usize; max_rank + 1];
for (r, tracks) in bus_tracks.iter_mut().enumerate().take(max_rank) {
let spans = bus_spans(graph, ranks, ¢ers, r, CrossAxisAlignment::Near);
let exits: Vec<&SkipEdge> = skips.iter().filter(|skip| skip.source_rank == r).collect();
let joins: Vec<&SkipEdge> = skips
.iter()
.filter(|skip| skip.target_rank == r + 1)
.collect();
if spans.is_empty() && exits.is_empty() && joins.is_empty() {
continue;
}
let assigned = assign_bus_tracks(&spans, &exits, &joins);
for (idx, slot) in assigned.edges {
edge_bus[idx] = slot;
}
for (idx, slot) in assigned.joins {
join_slots[idx] = slot;
}
*tracks = assigned.tracks;
}
let rank_h: Vec<usize> = by_rank
.iter()
.map(|row| {
row.iter()
.map(|&i| sizes.box_h[i] + sizes.extra_h[i])
.max()
.unwrap_or(3)
})
.collect();
let label_rows = label_gap_rows(graph, ranks, edge_labels, max_rank);
let back_pad = max_extra_label_rows(graph, edge_labels, |edge| {
edge_route(edge, ranks) == EdgeRoute::Back
});
let mut rank_y = vec![0usize; max_rank + 1];
rank_y[0] = back_pad;
for r in 1..=max_rank {
let gap = GAP_Y.max(bus_tracks[r - 1] + 1) + label_rows[r - 1];
rank_y[r] = rank_y[r - 1] + rank_h[r - 1] + gap;
}
let canvas_h = rank_y[max_rank] + rank_h[max_rank];
let band_end: Vec<usize> = (0..=max_rank).map(|r| rank_y[r] + rank_h[r]).collect();
let mut diagram_w = 1;
for (r, row) in by_rank.iter().enumerate() {
for &idx in row {
let w = sizes.box_w[idx];
let h = sizes.box_h[idx];
let cx = centers[idx];
let x = cx.saturating_sub(w / 2);
let y = rank_y[r] + (rank_h[r] - h - sizes.extra_h[idx]) / 2;
placed[idx] = Placed {
x,
y,
w,
h,
cx,
cy: y + h / 2,
rank: r,
};
diagram_w = diagram_w.max(x + w);
if sizes.extra_h[idx] > 0 && sizes.self_label_w[idx] > 0 {
diagram_w = diagram_w.max(x + w + 2 + sizes.self_label_w[idx]);
}
}
}
let detours = assign_detour_lanes(graph, ranks, placed, LaneAxis::Vertical);
let back_label_w = graph
.edges
.iter()
.enumerate()
.filter(|(_, edge)| edge_route(edge, ranks) == EdgeRoute::Back)
.map(|(index, _)| label_block_width(&edge_labels[index]))
.max()
.unwrap_or(0);
let left_margin = back_lane_margin(detours.back_count, back_label_w);
if left_margin > 0 {
for node in placed.iter_mut() {
node.x += left_margin;
node.cx += left_margin;
}
diagram_w += left_margin;
for anchor in source_anchors.iter_mut() {
*anchor += left_margin;
}
}
let mut content_w = diagram_w;
for (index, e) in graph.edges.iter().enumerate() {
let lw = label_block_width(&edge_labels[index]);
if lw == 0 {
continue;
}
if matches!(edge_route(e, ranks), EdgeRoute::Adjacent | EdgeRoute::Skip) {
content_w = content_w.max(placed[e.to].cx + 2 + lw);
}
}
let (canvas_w, skip_base) = if detours.skip_count == 0 {
(content_w, 0)
} else {
(content_w + 1 + detours.skip_count, content_w + 1)
};
let mut edge_join = vec![0usize; graph.edges.len()];
for skip in &skips {
edge_join[skip.index] = band_end[skip.target_rank - 1] + join_slots[skip.index];
}
RoutePlan {
canvas: (canvas_w, canvas_h),
band_end,
edge_bus,
source_anchors,
edge_lane: detours.into_absolute_lanes(graph, ranks, skip_base, 0),
edge_join,
}
}
pub(super) fn place_lr(
ranks: &[usize],
max_rank: usize,
by_rank: &[Vec<usize>],
sizes: &NodeSizes,
graph: &Graph,
edge_labels: &[Vec<String>],
placed: &mut [Placed],
) -> RoutePlan {
let col_w: Vec<usize> = by_rank
.iter()
.map(|row| row.iter().map(|&i| sizes.box_w[i]).max().unwrap_or(0))
.collect();
let max_label = graph
.edges
.iter()
.enumerate()
.filter(|(_, e)| {
matches!(
edge_route(e, ranks),
EdgeRoute::SelfLoop | EdgeRoute::Adjacent
)
})
.filter_map(|(index, e)| {
if e.from == e.to {
e.label.as_ref().map(|l| l.width().min(MAX_LABEL))
} else {
Some(label_block_width(&edge_labels[index])).filter(|&w| w > 0)
}
})
.max()
.unwrap_or(0);
let base_gap = (GAP_X + 1).max(max_label + 3);
let forward_pad = max_extra_label_rows(graph, edge_labels, |edge| {
edge_route(edge, ranks) == EdgeRoute::Adjacent
});
let skip_pad = max_extra_label_rows(graph, edge_labels, |edge| {
edge_route(edge, ranks) == EdgeRoute::Skip
});
let back_pad = max_extra_label_rows(graph, edge_labels, |edge| {
edge_route(edge, ranks) == EdgeRoute::Back
});
let centers = assign_positions(
by_rank,
&sizes.lay_h,
1.max(forward_pad),
&graph.edges,
ranks,
);
let mut edge_bus = vec![0usize; graph.edges.len()];
let mut bus_tracks = vec![0usize; max_rank + 1];
for (r, tracks) in bus_tracks.iter_mut().enumerate().take(max_rank) {
let spans = bus_spans(graph, ranks, ¢ers, r, CrossAxisAlignment::Exact);
if spans.is_empty() {
continue;
}
let assigned = assign_bus_tracks(&spans, &[], &[]);
for (idx, slot) in assigned.edges {
edge_bus[idx] = slot;
}
*tracks = assigned.tracks;
}
let mut rank_x = vec![0usize; max_rank + 1];
for r in 1..=max_rank {
let gap = base_gap.max(bus_tracks[r - 1] + 1);
rank_x[r] = rank_x[r - 1] + col_w[r - 1] + gap;
}
let mut canvas_w = rank_x[max_rank]
+ col_w[max_rank]
+ by_rank[max_rank]
.iter()
.filter(|&&i| sizes.extra_h[i] > 0 && sizes.self_label_w[i] > 0)
.map(|&i| 2 + sizes.self_label_w[i])
.max()
.unwrap_or(0);
let back_label_w = graph
.edges
.iter()
.enumerate()
.filter(|(_, edge)| matches!(edge_route(edge, ranks), EdgeRoute::Skip | EdgeRoute::Back))
.map(|(index, _)| label_block_width(&edge_labels[index]))
.max()
.unwrap_or(0);
if back_label_w > 0 {
canvas_w += 1 + back_label_w;
}
let band_end: Vec<usize> = (0..=max_rank).map(|r| rank_x[r] + col_w[r]).collect();
let mut diagram_h = 1;
for (r, row) in by_rank.iter().enumerate() {
let x = rank_x[r];
for &idx in row {
let w = sizes.box_w[idx];
let h = sizes.box_h[idx];
let cy = centers[idx];
let y = cy.saturating_sub((h + sizes.extra_h[idx]) / 2) + forward_pad;
placed[idx] = Placed {
x,
y,
w,
h,
cx: x + w / 2,
cy: y + h / 2,
rank: r,
};
diagram_h = diagram_h.max(y + h + sizes.extra_h[idx]);
}
}
let mut source_anchors: Vec<usize> = placed.iter().map(|node| node.cy).collect();
let detours = assign_detour_lanes(graph, ranks, placed, LaneAxis::Horizontal);
let mut back_base = 0;
if detours.back_count > 0 {
back_base = if back_pad == 0 { 0 } else { back_pad + 1 };
let top_margin = back_base + detours.back_count + 1;
for node in placed.iter_mut() {
node.y += top_margin;
node.cy += top_margin;
}
diagram_h += top_margin;
for anchor in source_anchors.iter_mut() {
*anchor += top_margin;
}
}
let (canvas_h, skip_base) = if detours.skip_count == 0 {
(diagram_h, 0)
} else {
(
diagram_h + skip_pad + 1 + detours.skip_count,
diagram_h + skip_pad + 1,
)
};
RoutePlan {
canvas: (canvas_w, canvas_h),
band_end,
edge_bus,
source_anchors,
edge_lane: detours.into_absolute_lanes(graph, ranks, skip_base, back_base),
edge_join: vec![0; graph.edges.len()],
}
}
pub(super) struct RoutePlan {
pub(super) canvas: (usize, usize),
pub(super) band_end: Vec<usize>,
pub(super) edge_bus: Vec<usize>,
pub(super) source_anchors: Vec<usize>,
pub(super) edge_lane: Vec<usize>,
pub(super) edge_join: Vec<usize>,
}
fn back_lane_margin(back_count: usize, back_label_w: usize) -> usize {
if back_count == 0 {
0
} else if back_label_w == 0 {
back_count + 1
} else {
back_count + back_label_w + 2
}
}
fn pack_tracks<T>(items: &[T], compatible: impl Fn(&T, &T) -> bool) -> (Vec<usize>, usize) {
let mut tracks: Vec<Vec<usize>> = Vec::new();
let mut slots = Vec::with_capacity(items.len());
for (index, item) in items.iter().enumerate() {
let fits = |members: &Vec<usize>| {
members
.iter()
.all(|&member| compatible(&items[member], item))
};
let slot = match tracks.iter().position(fits) {
Some(slot) => slot,
None => {
tracks.push(Vec::new());
tracks.len() - 1
}
};
tracks[slot].push(index);
slots.push(slot);
}
(slots, tracks.len())
}
fn assign_tracks(spans: &[LaneSpan]) -> (Vec<(usize, usize)>, usize) {
let mut sorted: Vec<&LaneSpan> = spans.iter().collect();
sorted.sort_by_key(|span| {
(
span.end.saturating_sub(span.start),
span.start,
span.end,
span.from,
span.to,
span.index,
)
});
let (slots, count) = pack_tracks(&sorted, |member, span| {
member.end + 2 <= span.start
|| span.end + 2 <= member.start
|| member.from == span.from
|| member.to == span.to
});
let out = sorted
.iter()
.zip(slots)
.map(|(span, slot)| (span.index, slot))
.collect();
(out, count)
}
struct DetourLanes {
edge_lane: Vec<usize>,
skip_count: usize,
back_count: usize,
}
fn assign_detour_lanes(
graph: &Graph,
ranks: &[usize],
placed: &[Placed],
axis: LaneAxis,
) -> DetourLanes {
let lanes = lane_spans(graph, ranks, placed, axis);
let (skip_spans, back_spans): (Vec<LaneSpan>, Vec<LaneSpan>) = lanes
.into_iter()
.partition(|span| edge_route(&graph.edges[span.index], ranks) == EdgeRoute::Skip);
let mut edge_lane = vec![0usize; graph.edges.len()];
let (skip_assigned, skip_count) = assign_tracks(&skip_spans);
for (idx, slot) in skip_assigned {
edge_lane[idx] = slot;
}
let (back_assigned, back_count) = assign_tracks(&back_spans);
for (idx, slot) in back_assigned {
edge_lane[idx] = back_count - 1 - slot;
}
DetourLanes {
edge_lane,
skip_count,
back_count,
}
}
impl DetourLanes {
fn into_absolute_lanes(
mut self,
graph: &Graph,
ranks: &[usize],
skip_base: usize,
back_base: usize,
) -> Vec<usize> {
for (index, edge) in graph.edges.iter().enumerate() {
match edge_route(edge, ranks) {
EdgeRoute::Skip => self.edge_lane[index] += skip_base,
EdgeRoute::Back => self.edge_lane[index] += back_base,
EdgeRoute::Adjacent | EdgeRoute::SelfLoop => {}
}
}
self.edge_lane
}
}
struct BusGroup {
start: usize,
end: usize,
sources: Vec<usize>,
edges: Vec<usize>,
joins: Vec<usize>,
exits: bool,
}
impl BusGroup {
fn empty() -> Self {
Self {
start: usize::MAX,
end: 0,
sources: Vec::new(),
edges: Vec::new(),
joins: Vec::new(),
exits: false,
}
}
fn bounded_end(&self) -> Option<usize> {
(self.joins.is_empty() && !self.exits).then_some(self.end)
}
}
struct BusAssignment {
edges: Vec<(usize, usize)>,
joins: Vec<(usize, usize)>,
tracks: usize,
}
fn assign_bus_tracks(
fan_in: &[BusSpan],
skip_exits: &[&SkipEdge],
skip_joins: &[&SkipEdge],
) -> BusAssignment {
let mut by_target: BTreeMap<usize, BusGroup> = BTreeMap::new();
for span in fan_in {
let group = by_target.entry(span.to).or_insert_with(BusGroup::empty);
group.sources.push(span.from);
if span.jogs {
group.start = group.start.min(span.start);
group.end = group.end.max(span.end);
group.edges.push(span.index);
}
}
for skip in skip_joins {
let group = by_target.entry(skip.to).or_insert_with(BusGroup::empty);
group.sources.push(skip.from);
group.start = group.start.min(skip.target_center);
group.joins.push(skip.index);
}
let mut merged: Vec<BusGroup> = Vec::new();
for (_, mut group) in by_target {
if group.edges.is_empty() && group.joins.is_empty() {
continue;
}
group.sources.sort_unstable();
group.sources.dedup();
match merged
.iter_mut()
.find(|existing| existing.sources == group.sources)
{
Some(existing) => {
existing.start = existing.start.min(group.start);
existing.end = existing.end.max(group.end);
existing.edges.extend(group.edges);
existing.joins.extend(group.joins);
}
None => merged.push(group),
}
}
let mut by_source: BTreeMap<usize, BusGroup> = BTreeMap::new();
for skip in skip_exits {
match merged.iter_mut().find(|group| group.sources == [skip.from]) {
Some(group) => {
group.start = group.start.min(skip.source_center);
group.edges.push(skip.index);
group.exits = true;
}
None => {
let group = by_source.entry(skip.from).or_insert_with(|| BusGroup {
sources: vec![skip.from],
exits: true,
..BusGroup::empty()
});
group.start = group.start.min(skip.source_center);
group.edges.push(skip.index);
}
}
}
let mut groups = merged;
groups.extend(by_source.into_values());
groups.sort_by_key(|group| {
(
group.start,
group.bounded_end().is_none(),
group.bounded_end(),
group.edges.first().or(group.joins.first()).copied(),
)
});
let (slots, tracks) = pack_tracks(&groups, |member, group| {
match (member.bounded_end(), group.bounded_end()) {
(Some(end), Some(group_end)) => end + 2 <= group.start || group_end + 2 <= member.start,
(None, Some(group_end)) => group_end + 2 <= member.start,
(Some(end), None) => end + 2 <= group.start,
(None, None) => false,
}
});
let mut out = BusAssignment {
edges: Vec::new(),
joins: Vec::new(),
tracks,
};
for (group, slot) in groups.iter().zip(slots) {
for &idx in &group.edges {
out.edges.push((idx, slot));
}
for &idx in &group.joins {
out.joins.push((idx, slot));
}
}
out
}