use crate::graph::{Direction, EdgeKind, Graph, Node};
use crate::orientation::{is_before, OrientedCoords};
use crate::spacing::SpacingConfig;
use crate::style::StyleChars;
use super::canvas::Canvas;
use super::provenance::edge_owner_id;
use super::semantic::CellOwnerKind;
use super::{is_textual, stamp_portal_opening, subgraph_title_y, title_span};
const ROUTE_Z_INDEX: u8 = 5;
#[derive(Copy, Clone)]
struct RouteOwner<'a> {
kind: CellOwnerKind,
id: &'a str,
}
pub fn route_divergent_edges(
from: &Node,
to_nodes: &[&Node],
canvas: &mut Canvas,
style: &StyleChars,
spacing: &SpacingConfig,
direction: Direction,
graph: &Graph,
) {
if to_nodes.is_empty() || !canvas.is_visible(from) {
return;
}
let coords = OrientedCoords::new(direction);
let debug_timing = std::env::var("TERMIFLOW_DEBUG_TIMING").is_ok();
if debug_timing {
let targets: Vec<&str> = to_nodes.iter().map(|n| n.id.as_str()).collect();
eprintln!("render: route from {} to {:?}", from.id, targets);
}
let visible_targets: Vec<&Node> = to_nodes
.iter()
.filter(|n| canvas.is_visible(n))
.copied()
.collect();
if visible_targets.is_empty() {
return;
}
let (src_x, src_y) = get_node_center(from);
let (stem_start_x, stem_start_y) = edge_exit_point(from, direction);
let stem_length = match direction {
Direction::LR | Direction::RL => spacing.stem_length_horizontal,
_ => spacing.stem_length_vertical,
};
let (mut junction_x, mut junction_y) = coords.advance(stem_start_x, stem_start_y, stem_length);
let mut target_positions: Vec<(usize, usize, &Node)> = visible_targets
.iter()
.map(|&n| {
let (tx, ty) = get_node_center(n);
(tx, ty, n)
})
.collect();
target_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
if matches!(direction, Direction::TD | Direction::TB | Direction::BT)
&& target_positions.len() > 1
{
if let Some(target_sg) = visible_targets
.first()
.and_then(|n| graph.get_node_subgraph(&n.id))
{
let all_same = visible_targets
.iter()
.all(|n| graph.get_node_subgraph(&n.id) == Some(target_sg));
let source_sg = graph.get_node_subgraph(&from.id);
if all_same && source_sg != Some(target_sg) {
if let Some(sg) = graph.get_subgraph(target_sg) {
match direction {
Direction::TD | Direction::TB => route_divergent_into_subgraph_td(
from,
&visible_targets,
canvas,
style,
sg,
direction,
graph,
),
Direction::BT => route_divergent_into_subgraph_bt(
from,
&visible_targets,
canvas,
style,
sg,
graph,
),
_ => unreachable!(),
}
return;
}
}
}
}
if matches!(direction, Direction::LR | Direction::RL) && target_positions.len() > 1 {
let arrow_primaries: Vec<usize> = target_positions
.iter()
.map(|(_, _, n)| {
let (ax, ay) = adjusted_edge_entry_point(n, direction, graph);
coords.primary_coord(ax, ay)
})
.collect();
if let Some(closest_arrow) = match direction {
Direction::LR => arrow_primaries.iter().min(),
Direction::RL => arrow_primaries.iter().max(),
_ => None,
} {
let stem_start_primary = coords.primary_coord(stem_start_x, stem_start_y);
let current_primary = coords.primary_coord(junction_x, junction_y);
let desired_primary = match direction {
Direction::LR => closest_arrow.saturating_sub(3),
Direction::RL => closest_arrow.saturating_add(3),
_ => current_primary,
};
let adjusted_primary = match direction {
Direction::LR => desired_primary
.min(current_primary)
.max(stem_start_primary + 1),
Direction::RL => desired_primary
.max(current_primary)
.min(stem_start_primary.saturating_sub(1)),
_ => current_primary,
};
if adjusted_primary != current_primary {
coords.set_primary(&mut junction_x, &mut junction_y, adjusted_primary);
}
}
}
if matches!(direction, Direction::LR | Direction::RL) && target_positions.len() > 1 {
let stem_start_primary = coords.primary_coord(stem_start_x, stem_start_y);
let junction_primary = coords.primary_coord(junction_x, junction_y);
let nearest_arrow_primary = target_positions
.iter()
.map(|(_, _, n)| {
let (ax, ay) = adjusted_edge_entry_point(n, direction, graph);
coords.primary_coord(ax, ay)
})
.min_by_key(|p| junction_primary.abs_diff(*p));
if let Some(arrow_primary) = nearest_arrow_primary {
let gap = junction_primary.abs_diff(arrow_primary);
let min_gap = 3;
if gap < min_gap {
let adjust = min_gap - gap;
let mut adjusted_primary = junction_primary;
match direction {
Direction::LR => {
adjusted_primary = adjusted_primary.saturating_sub(adjust);
adjusted_primary = adjusted_primary.max(stem_start_primary + 1);
}
Direction::RL => {
adjusted_primary = adjusted_primary.saturating_add(adjust);
adjusted_primary = adjusted_primary
.min(stem_start_primary.saturating_sub(1).max(adjusted_primary));
}
_ => {}
}
coords.set_primary(&mut junction_x, &mut junction_y, adjusted_primary);
}
}
}
if target_positions.len() == 1 {
let (target_x, target_y, target) = (
target_positions[0].0,
target_positions[0].1,
target_positions[0].2,
);
let route_owner_id = edge_route_owner_id(graph, &from.id, &target.id);
let route_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: route_owner_id.as_str(),
};
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
if matches!(direction, Direction::TD | Direction::TB) {
let from_sg = graph.get_node_subgraph(&from.id);
let to_sg = graph.get_node_subgraph(&target.id);
if std::env::var("DEBUG_CROSS").is_ok() {
eprintln!(
"single-edge cross? {}({:?}) -> {}({:?})",
from.id, from_sg, target.id, to_sg
);
}
if from_sg != to_sg
&& route_cross_subgraph_td(
from,
target,
stem_start_x,
stem_start_y,
arrow_x,
arrow_y,
canvas,
style,
graph,
Some(route_owner),
)
{
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(route_owner),
);
return;
}
} else if direction == Direction::BT {
let from_sg = graph.get_node_subgraph(&from.id);
let to_sg = graph.get_node_subgraph(&target.id);
if from_sg != to_sg
&& route_cross_subgraph_bt(
from,
target,
stem_start_x,
stem_start_y,
arrow_x,
arrow_y,
canvas,
style,
graph,
Some(route_owner),
)
{
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(route_owner),
);
return;
}
}
if debug_timing {
eprintln!(
" single target centers ({},{}) -> ({},{})",
src_x, src_y, arrow_x, arrow_y
);
}
let src_secondary = coords.secondary_coord(src_x, src_y);
let target_secondary = coords.secondary_coord(target_x, target_y);
if src_secondary == target_secondary {
draw_line_primary(
stem_start_x,
stem_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
if matches!(direction, Direction::TD | Direction::TB) {
if let (Some(from_sg), Some(to_sg)) = (
graph.get_node_subgraph(&from.id),
graph.get_node_subgraph(&target.id),
) {
if from_sg != to_sg {
if let Some(sg) = graph.get_subgraph(to_sg) {
let border_y = sg.bounds.y;
if arrow_x < canvas.width && border_y < canvas.height && !sg.has_title()
{
set_route_edge_char(
canvas,
arrow_x,
border_y,
style.junction_down,
style,
Some(route_owner),
);
}
}
}
}
}
} else {
let going_before = is_before(src_secondary, target_secondary);
if matches!(direction, Direction::LR | Direction::RL) {
if target_secondary != src_secondary {
let spine_x = junction_x;
draw_line_primary(
stem_start_x,
stem_start_y,
spine_x,
stem_start_y,
&coords,
canvas,
style,
Some(graph),
None,
);
let going_up = target_secondary < src_secondary;
let corner1 = match direction {
Direction::LR => {
if going_up {
style.corner_ur
} else {
style.corner_dr
}
}
Direction::RL => {
if going_up {
style.corner_ul
} else {
style.corner_dl
}
}
_ => unreachable!(),
};
set_route_edge_char(
canvas,
spine_x,
stem_start_y,
corner1,
style,
Some(route_owner),
);
let (bend_x, bend_y) =
coords.with_secondary(spine_x, stem_start_y, target_secondary);
draw_line_secondary(
spine_x,
stem_start_y,
bend_x,
bend_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
let corner2 = match direction {
Direction::LR => {
if going_up {
style.corner_dl
} else {
style.corner_ul
}
}
Direction::RL => {
if going_up {
style.corner_dr
} else {
style.corner_ur
}
}
_ => unreachable!(),
};
set_route_edge_char(canvas, bend_x, bend_y, corner2, style, Some(route_owner));
let (seg_start_x, seg_start_y) = coords.advance(bend_x, bend_y, 1);
draw_line_primary(
seg_start_x,
seg_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(route_owner),
);
return;
}
let (bend_x, bend_y) =
coords.with_secondary(stem_start_x, stem_start_y, target_secondary);
draw_line_secondary(
stem_start_x,
stem_start_y,
bend_x,
bend_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
let corner = coords.corner_secondary_to_end(going_before, style);
set_route_edge_char(canvas, bend_x, bend_y, corner, style, Some(route_owner));
let (seg_start_x, seg_start_y) = coords.advance(bend_x, bend_y, 1);
draw_line_primary(
seg_start_x,
seg_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
} else {
if matches!(direction, Direction::BT) {
let (bend_x, _bend_y) =
coords.with_secondary(junction_x, junction_y, target_secondary);
let (x0, x1) = if junction_x <= bend_x {
(junction_x, bend_x)
} else {
(bend_x, junction_x)
};
let span_conflicts = if x1 > x0 + 1 {
((x0 + 1)..x1).any(|x| canvas.get(x, junction_y) != ' ')
} else {
false
};
let junction_cell = canvas.get(junction_x, junction_y);
let junction_conflicts =
junction_cell != ' ' && !super::canvas::is_vertical(junction_cell, style);
if span_conflicts {
let (cand_x, cand_y) = coords.retreat(junction_x, junction_y, 1);
let stem_start_primary = coords.primary_coord(stem_start_x, stem_start_y);
let cand_primary = coords.primary_coord(cand_x, cand_y);
if cand_primary <= stem_start_primary {
let (cand_bx, _) =
coords.with_secondary(cand_x, cand_y, target_secondary);
let (cx0, cx1) = if cand_x <= cand_bx {
(cand_x, cand_bx)
} else {
(cand_bx, cand_x)
};
let cand_conflicts = if cx1 > cx0 + 1 {
((cx0 + 1)..cx1).any(|x| canvas.get(x, cand_y) != ' ')
} else {
false
};
if !cand_conflicts {
junction_x = cand_x;
junction_y = cand_y;
}
}
} else if junction_conflicts {
let (cand_x, cand_y) = coords.retreat(junction_x, junction_y, 1);
let stem_start_primary = coords.primary_coord(stem_start_x, stem_start_y);
if coords.primary_coord(cand_x, cand_y) <= stem_start_primary {
junction_x = cand_x;
junction_y = cand_y;
}
}
}
draw_line_primary(
stem_start_x,
stem_start_y,
junction_x,
junction_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
let corner = coords.corner_start_to_secondary(going_before, style);
set_route_edge_char(
canvas,
junction_x,
junction_y,
corner,
style,
Some(route_owner),
);
let (bend_x, bend_y) =
coords.with_secondary(junction_x, junction_y, target_secondary);
draw_line_secondary(
junction_x,
junction_y,
bend_x,
bend_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
let corner2 = coords.corner_secondary_to_end(going_before, style);
set_route_edge_char(canvas, bend_x, bend_y, corner2, style, Some(route_owner));
let (seg_start_x, seg_start_y) = coords.advance(bend_x, bend_y, 1);
draw_line_primary(
seg_start_x,
seg_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(route_owner),
);
}
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(route_owner),
);
if matches!(direction, Direction::TD | Direction::TB) {
if let Some(from_sg) = graph.get_node_subgraph(&from.id) {
if graph.get_node_subgraph(&target.id) != Some(from_sg) {
if let Some(sg) = graph.get_subgraph(from_sg) {
let border_y = sg.bounds.y + sg.bounds.height.saturating_sub(1);
if arrow_x < canvas.width && border_y < canvas.height {
set_route_edge_char(
canvas,
arrow_x,
border_y,
style.junction_down,
style,
Some(route_owner),
);
}
}
}
}
}
return;
}
if matches!(direction, Direction::TD | Direction::TB) {
if let Some(target_sg_id) = target_positions
.first()
.and_then(|(_, _, n)| graph.get_node_subgraph(&n.id))
{
let source_sg = graph.get_node_subgraph(&from.id);
let all_targets_same_sg = target_positions
.iter()
.all(|(_, _, n)| graph.get_node_subgraph(&n.id) == Some(target_sg_id));
if all_targets_same_sg && source_sg != Some(target_sg_id) {
if let Some(sg) = graph.get_subgraph(target_sg_id) {
if sg.bounds.is_valid() {
route_fanout_into_subgraph_td(
from,
&target_positions,
canvas,
style,
sg,
direction,
graph,
);
return;
}
}
}
}
}
let fanout_owner_id = format!("fanout:{}", from.id);
let fanout_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanout_owner_id.as_str(),
};
let stem_length = {
let start_primary = coords.primary_coord(stem_start_x, stem_start_y);
let junction_primary = coords.primary_coord(junction_x, junction_y);
match direction {
Direction::LR | Direction::TD | Direction::TB => {
junction_primary.saturating_sub(start_primary)
}
Direction::RL | Direction::BT => start_primary.saturating_sub(junction_primary),
}
};
for i in 0..stem_length {
let (px, py) = coords.advance(stem_start_x, stem_start_y, i);
set_route_edge_char(
canvas,
px,
py,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
let src_secondary = coords.secondary_coord(src_x, src_y);
let target_secondaries: Vec<usize> = target_positions
.iter()
.map(|(_, _, target)| {
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
coords.secondary_coord(arrow_x, arrow_y)
})
.collect();
let first_secondary = target_secondaries
.iter()
.copied()
.min()
.unwrap_or(src_secondary);
let last_secondary = target_secondaries
.iter()
.copied()
.max()
.unwrap_or(src_secondary);
let span_start = first_secondary;
let span_end = last_secondary;
let mut junction_secondary = src_secondary;
if span_end > span_start {
if junction_secondary == span_start {
junction_secondary = span_start + 1;
} else if junction_secondary == span_end {
junction_secondary = span_end - 1;
}
}
let (start_corner, end_corner) = match direction {
Direction::TD | Direction::TB => (style.corner_dl, style.corner_dr),
Direction::BT => (style.corner_ul, style.corner_ur),
Direction::LR => (style.corner_dl, style.corner_ul),
Direction::RL => (style.corner_dr, style.corner_ur),
};
let has_target_at_junction = target_positions
.iter()
.any(|(x, y, _)| coords.secondary_coord(*x, *y) == junction_secondary);
for pos in span_start..=span_end {
let (span_x, span_y) = coords.with_secondary(junction_x, junction_y, pos);
let is_target_drop = pos != junction_secondary && target_secondaries.contains(&pos);
let c = if pos == junction_secondary {
match direction {
Direction::TD | Direction::TB => style.junction_up, Direction::LR => {
if has_target_at_junction {
style.junction_right } else {
style.junction_left }
}
Direction::RL => {
if has_target_at_junction {
style.junction_left } else {
style.junction_right }
}
Direction::BT => style.junction_down, }
} else if pos == span_start {
start_corner
} else if pos == span_end {
end_corner
} else if is_target_drop {
match direction {
Direction::TD | Direction::TB => style.junction_down, Direction::BT => style.junction_up, Direction::LR => style.junction_right, Direction::RL => style.junction_left, }
} else {
coords.secondary_edge_char(style)
};
set_route_edge_char(canvas, span_x, span_y, c, style, Some(fanout_owner));
}
if junction_secondary != src_secondary {
let (sx, sy) = coords.with_secondary(junction_x, junction_y, src_secondary);
let (jx, jy) = coords.with_secondary(junction_x, junction_y, junction_secondary);
draw_line_secondary(
sx,
sy,
jx,
jy,
&coords,
canvas,
style,
Some(graph),
Some(fanout_owner),
);
}
for (_, _, target) in &target_positions {
let branch_owner_id = edge_route_owner_id(graph, &from.id, &target.id);
let branch_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: branch_owner_id.as_str(),
};
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
let target_secondary = coords.secondary_coord(arrow_x, arrow_y);
let (drop_x, drop_y) = coords.with_secondary(junction_x, junction_y, target_secondary);
let (drop_start_x, drop_start_y) = coords.advance(drop_x, drop_y, 1);
if drop_start_x != arrow_x || drop_start_y != arrow_y {
draw_line_primary(
drop_start_x,
drop_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(branch_owner),
);
}
let edge_kind = graph
.edges
.iter()
.find(|e| e.from == from.id && e.to == target.id && !e.is_back_edge)
.map(|e| e.kind)
.unwrap_or(EdgeKind::Arrow);
let tip = match edge_kind {
EdgeKind::CircleEnd => style.circle_end,
EdgeKind::CrossEnd => style.cross_end,
EdgeKind::Open => coords.primary_edge_char(style), _ => coords.arrow_end(style),
};
set_route_char(canvas, arrow_x, arrow_y, tip, Some(branch_owner));
}
let (start_pos_x, start_pos_y) = coords.with_secondary(junction_x, junction_y, span_start);
let (end_pos_x, end_pos_y) = coords.with_secondary(junction_x, junction_y, span_end);
let primary_edge = coords.primary_edge_char(style);
if span_start != junction_secondary {
let existing = canvas.get(start_pos_x, start_pos_y);
if existing == primary_edge || existing == ' ' {
set_route_char(
canvas,
start_pos_x,
start_pos_y,
start_corner,
Some(fanout_owner),
);
} else {
set_route_edge_char(
canvas,
start_pos_x,
start_pos_y,
start_corner,
style,
Some(fanout_owner),
);
}
}
if span_end != junction_secondary {
let existing = canvas.get(end_pos_x, end_pos_y);
if existing == primary_edge || existing == ' ' {
set_route_char(canvas, end_pos_x, end_pos_y, end_corner, Some(fanout_owner));
} else {
set_route_edge_char(
canvas,
end_pos_x,
end_pos_y,
end_corner,
style,
Some(fanout_owner),
);
}
}
}
fn route_fanout_into_subgraph_td(
from: &Node,
targets: &[(usize, usize, &Node)],
canvas: &mut Canvas,
style: &StyleChars,
sg: &crate::graph::Subgraph,
direction: Direction,
graph: &Graph,
) {
let coords = OrientedCoords::new(direction);
let (stem_start_x, stem_start_y) = edge_exit_point(from, direction);
let fanout_owner_id = format!("fanout:{}", from.id);
let fanout_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanout_owner_id.as_str(),
};
let portal_center = sg.bounds.x + sg.bounds.width / 2;
let min_target_x = targets
.iter()
.map(|(x, _, _)| *x)
.min()
.unwrap_or(portal_center);
let max_target_x = targets
.iter()
.map(|(x, _, _)| *x)
.max()
.unwrap_or(portal_center);
let junction_x = portal_center.clamp(min_target_x, max_target_x);
let portal_y = sg.bounds.y.saturating_add(1);
let min_arrow_y = targets
.iter()
.map(|(_, _, t)| adjusted_edge_entry_point(t, direction, graph).1)
.min()
.unwrap_or(portal_y.saturating_add(2));
if std::env::var("DEBUG_FANOUT").is_ok() {
let target_xs: Vec<usize> = targets.iter().map(|(x, _, _)| *x).collect();
eprintln!(
"fanout stem=({}, {}) portal_y={} jx={} targets={:?}",
stem_start_x, stem_start_y, portal_y, junction_x, target_xs
);
}
let delta = min_arrow_y.saturating_sub(portal_y);
let mut junction_y = portal_y.saturating_add(delta / 2);
let min_split_y = portal_y.saturating_add(1);
let max_split_y = min_arrow_y.saturating_sub(2).max(min_split_y);
if junction_y < min_split_y {
junction_y = min_split_y;
} else if junction_y > max_split_y {
junction_y = max_split_y;
}
if stem_start_x != junction_x {
let (hx0, hx1) = if stem_start_x < junction_x {
(stem_start_x, junction_x)
} else {
(junction_x, stem_start_x)
};
for x in hx0..=hx1 {
set_route_edge_char(
canvas,
x,
stem_start_y,
style.edge_h,
style,
Some(fanout_owner),
);
}
let corner = if junction_x > stem_start_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(
canvas,
junction_x,
stem_start_y,
corner,
style,
Some(fanout_owner),
);
} else {
set_route_edge_char(
canvas,
junction_x,
stem_start_y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
if stem_start_y < junction_y {
for y in (stem_start_y + 1)..=junction_y {
set_route_edge_char(
canvas,
junction_x,
y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
}
let mut sorted_targets = targets.to_vec();
sorted_targets.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let first_secondary = coords.secondary_coord(sorted_targets[0].0, sorted_targets[0].1);
let last_secondary = coords.secondary_coord(
sorted_targets[sorted_targets.len() - 1].0,
sorted_targets[sorted_targets.len() - 1].1,
);
let junction_secondary = coords.secondary_coord(junction_x, junction_y);
if junction_y > portal_y {
let spine_y = junction_y.saturating_sub(1);
set_route_edge_char(
canvas,
junction_x,
spine_y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
let span_start = first_secondary.min(junction_secondary);
let span_end = last_secondary.max(junction_secondary);
for pos in span_start..=span_end {
let (span_x, span_y) = coords.with_secondary(junction_x, junction_y, pos);
let c = if pos == junction_secondary {
match direction {
Direction::TD | Direction::TB => style.junction_up,
Direction::LR => style.junction_left,
Direction::RL => style.junction_right,
Direction::BT => style.junction_down,
}
} else if pos == span_start {
match direction {
Direction::TD | Direction::TB => style.corner_dl,
Direction::LR => style.corner_dl,
Direction::RL => style.corner_dr,
Direction::BT => style.corner_ul,
}
} else if pos == span_end {
match direction {
Direction::TD | Direction::TB => style.corner_dr,
Direction::LR => style.corner_ul,
Direction::RL => style.corner_ur,
Direction::BT => style.corner_ur,
}
} else {
coords.secondary_edge_char(style)
};
set_route_edge_char(canvas, span_x, span_y, c, style, Some(fanout_owner));
}
if matches!(direction, Direction::TD | Direction::TB) {
set_route_char(
canvas,
junction_x,
junction_y,
style.junction_up,
Some(fanout_owner),
);
}
for (target_x, target_y, target) in &sorted_targets {
let branch_owner_id = edge_route_owner_id(graph, &from.id, &target.id);
let branch_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: branch_owner_id.as_str(),
};
let target_secondary = coords.secondary_coord(*target_x, *target_y);
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
let (drop_x, drop_y) = coords.with_secondary(junction_x, junction_y, target_secondary);
let (drop_start_x, drop_start_y) = coords.advance(drop_x, drop_y, 1);
if drop_start_x != arrow_x || drop_start_y != arrow_y {
draw_line_primary(
drop_start_x,
drop_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
None,
Some(branch_owner),
);
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(branch_owner),
);
}
}
fn route_convergent_from_subgraph_td(
sources: &[&Node],
target: &Node,
canvas: &mut Canvas,
style: &StyleChars,
sg: &crate::graph::Subgraph,
direction: Direction,
graph: &Graph,
) {
let coords = OrientedCoords::new(direction);
let (target_x, target_y) = get_node_center(target);
let fanin_owner_id = format!("fanin:{}", target.id);
let fanin_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanin_owner_id.as_str(),
};
let max_exit_y = sources
.iter()
.map(|n| edge_exit_point(n, direction).1)
.max()
.unwrap_or(0);
let bottom_limit = sg.bounds.y + sg.bounds.height.saturating_sub(1);
let mut merge_y = bottom_limit.saturating_sub(3);
merge_y = merge_y.max(max_exit_y.saturating_add(1));
merge_y = merge_y.min(bottom_limit.saturating_sub(1));
let outer_container = smallest_visual_container(graph, sg, target);
let mut source_positions: Vec<(usize, usize, &Node)> = sources
.iter()
.map(|n| {
let (sx, sy) = get_node_center(n);
(sx, sy, *n)
})
.collect();
source_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let nested_source_center = if matches!(direction, Direction::TD | Direction::TB)
&& outer_container.is_some()
&& !source_positions.is_empty()
{
let span_start = source_positions
.iter()
.map(|(x, y, _)| coords.secondary_coord(*x, *y))
.min()
.unwrap_or(target_x);
let span_end = source_positions
.iter()
.map(|(x, y, _)| coords.secondary_coord(*x, *y))
.max()
.unwrap_or(target_x);
let span_center = (span_start + span_end) / 2;
Some((span_center.saturating_mul(3) + target_x) / 4)
} else {
None
};
let preferred_merge_x = match direction {
Direction::TD | Direction::TB => {
let inset = if outer_container.is_some() && sg.bounds.width >= 9 {
3
} else {
1
};
let min_x = sg.bounds.x.saturating_add(inset);
let max_x = sg
.bounds
.x
.saturating_add(sg.bounds.width.saturating_sub(inset + 1));
nested_source_center
.unwrap_or(target_x)
.clamp(min_x, max_x.max(min_x))
}
_ => sg.bounds.x + sg.bounds.width / 2,
};
let (arrow_x, arrow_y) = edge_entry_candidates(target, direction)
.into_iter()
.filter(|(candidate_x, candidate_y)| {
!hits_foreign_subgraph_border(target, *candidate_x, *candidate_y, graph)
})
.min_by_key(|(candidate_x, _)| candidate_x.abs_diff(preferred_merge_x))
.unwrap_or_else(|| adjusted_edge_entry_point(target, direction, graph));
let merge_x = if let Some(outer) = outer_container {
let relay_y = outer.bounds.y + outer.bounds.height;
if relay_y == arrow_y && preferred_merge_x.abs_diff(arrow_x) <= 4 {
arrow_x
} else {
preferred_merge_x
}
} else {
preferred_merge_x
};
let target_secondary = coords.secondary_coord(target_x, target_y);
let (span_start, span_end) = draw_source_lines_to_merge(
&source_positions,
merge_x,
merge_y,
&coords,
canvas,
style,
direction,
Some(graph),
Some(&target.id),
);
let (final_span_start, final_span_end) = if matches!(direction, Direction::TD | Direction::TB) {
let merge_secondary = coords.secondary_coord(merge_x, merge_y);
(
span_start.min(merge_secondary),
span_end.max(merge_secondary),
)
} else {
(
span_start.min(target_secondary),
span_end.max(target_secondary),
)
};
if std::env::var("DEBUG_FANIN").is_ok() {
eprintln!(
"fanin merge_x={} merge_y={} span=({}, {}) target_sec={} target=({}, {}) arrow=({}, {})",
merge_x,
merge_y,
final_span_start,
final_span_end,
target_secondary,
target_x,
target_y,
arrow_x,
arrow_y
);
}
draw_merge_line(
merge_x,
merge_y,
final_span_start,
final_span_end,
&coords,
canvas,
style,
Some(fanin_owner),
);
match direction {
Direction::BT => {
let (sx, sy) = coords.with_secondary(merge_x, merge_y, final_span_start);
let (ex, ey) = coords.with_secondary(merge_x, merge_y, final_span_end);
set_route_edge_char(canvas, sx, sy, style.corner_ul, style, Some(fanin_owner));
set_route_edge_char(canvas, ex, ey, style.corner_ur, style, Some(fanin_owner));
}
Direction::TD | Direction::TB => {
let merge_secondary = coords.secondary_coord(merge_x, merge_y);
for pos in final_span_start..=final_span_end {
let (sx, sy) = coords.with_secondary(merge_x, merge_y, pos);
let ch = if pos == final_span_start {
style.corner_ul
} else if pos == final_span_end {
style.corner_ur
} else if pos == merge_secondary {
style.junction_down
} else {
coords.secondary_edge_char(style)
};
set_route_edge_char(canvas, sx, sy, ch, style, Some(fanin_owner));
}
}
_ => {}
}
let junction_char = match direction {
Direction::TD | Direction::TB => style.junction_down,
Direction::LR => style.junction_right,
Direction::RL => style.junction_left,
Direction::BT => style.junction_up,
};
set_route_char(canvas, merge_x, merge_y, junction_char, Some(fanin_owner));
let (mut cursor_x, mut cursor_y) = coords.advance(merge_x, merge_y, 1);
if let Some(outer) = outer_container {
let relay_y = outer.bounds.y + outer.bounds.height;
if relay_y >= cursor_y && relay_y <= arrow_y {
draw_line_primary(
cursor_x,
cursor_y,
cursor_x,
relay_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
cursor_y = relay_y;
let outer_exit_x = arrow_x;
if cursor_x != outer_exit_x {
let start_corner = if outer_exit_x > cursor_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(
canvas,
cursor_x,
cursor_y,
start_corner,
style,
Some(fanin_owner),
);
let (hx0, hx1) = if outer_exit_x > cursor_x {
(cursor_x + 1, outer_exit_x.saturating_sub(1))
} else {
(outer_exit_x + 1, cursor_x.saturating_sub(1))
};
for x in hx0..=hx1 {
if is_subgraph_title_cell(graph, x, cursor_y) {
continue;
}
set_route_edge_char(
canvas,
x,
cursor_y,
style.edge_h,
style,
Some(fanin_owner),
);
}
let end_corner = if outer_exit_x > cursor_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(
canvas,
outer_exit_x,
cursor_y,
end_corner,
style,
Some(fanin_owner),
);
cursor_x = outer_exit_x;
}
}
}
let turn_y = if cursor_x != arrow_x && arrow_y > cursor_y {
arrow_y
.saturating_sub(1)
.min(bottom_limit.saturating_sub(1))
.max(cursor_y)
} else {
arrow_y
};
draw_line_primary(
cursor_x,
cursor_y,
cursor_x,
turn_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
cursor_y = turn_y;
if cursor_x != arrow_x {
let start_corner = if arrow_x > cursor_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(
canvas,
cursor_x,
cursor_y,
start_corner,
style,
Some(fanin_owner),
);
let (hx0, hx1) = if cursor_x < arrow_x {
(cursor_x.saturating_add(1), arrow_x.saturating_sub(1))
} else {
(arrow_x.saturating_add(1), cursor_x.saturating_sub(1))
};
for x in hx0..=hx1 {
if is_subgraph_title_cell(graph, x, cursor_y) {
continue;
}
set_route_edge_char(canvas, x, cursor_y, style.edge_h, style, Some(fanin_owner));
}
let end_corner = if arrow_x > cursor_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(
canvas,
arrow_x,
cursor_y,
end_corner,
style,
Some(fanin_owner),
);
cursor_x = arrow_x;
}
if cursor_y < arrow_y {
draw_line_primary(
cursor_x,
cursor_y.saturating_add(1),
cursor_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
}
let bottom_y = sg.bounds.y + sg.bounds.height.saturating_sub(1);
if bottom_y < canvas.height {
let portal_columns: Vec<usize> = source_positions
.iter()
.map(|(sx, _, _)| coords.secondary_coord(*sx, bottom_y))
.collect();
let border_fill = sample_bottom_border_fill(
canvas,
sg,
bottom_y,
merge_x,
&portal_columns,
coords.secondary_edge_char(style),
);
if std::env::var("DEBUG_FANIN").is_ok() {
eprintln!(
"cleanup bottom_y={} fill='{}' portals={:?}",
bottom_y, border_fill, portal_columns
);
}
for (sx, sy, _) in &source_positions {
let sec = coords.secondary_coord(*sx, *sy);
let (px, py) = coords.with_secondary(merge_x, bottom_y, sec);
if px != merge_x && px < canvas.width {
canvas.set(px, py, border_fill);
}
}
if merge_x < canvas.width {
set_route_char(canvas, merge_x, bottom_y, style.edge_v, Some(fanin_owner));
}
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(fanin_owner),
);
}
fn sample_bottom_border_fill(
canvas: &Canvas,
sg: &crate::graph::Subgraph,
bottom_y: usize,
merge_x: usize,
portal_columns: &[usize],
fallback: char,
) -> char {
let left = sg.bounds.x.saturating_add(1);
let right = sg
.bounds
.x
.saturating_add(sg.bounds.width.saturating_sub(2));
for x in left..=right {
if x == merge_x || portal_columns.contains(&x) {
continue;
}
let ch = canvas.get(x, bottom_y);
if ch != ' ' {
return ch;
}
}
fallback
}
fn route_convergent_from_subgraph_bt(
sources: &[&Node],
target: &Node,
canvas: &mut Canvas,
style: &StyleChars,
sg: &crate::graph::Subgraph,
direction: Direction,
graph: &Graph,
) {
if direction != Direction::BT || sources.is_empty() || !sg.bounds.is_valid() {
return;
}
let coords = OrientedCoords::new(direction);
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
let fanin_owner_id = format!("fanin:{}", target.id);
let fanin_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanin_owner_id.as_str(),
};
let top_y = sg.bounds.y;
let _bottom_y = sg.bounds.y + sg.bounds.height.saturating_sub(1);
let inside_top = top_y.saturating_add(1);
let min_exit_y = sources
.iter()
.map(|n| edge_exit_point(n, direction).1)
.min()
.unwrap_or(inside_top.saturating_add(2));
let mut merge_y = min_exit_y.saturating_sub(2);
merge_y = merge_y.max(inside_top.saturating_add(1));
let merge_x = preferred_portal_x(
&sg.bounds,
sg.title.as_deref(),
arrow_x,
canvas,
direction,
false,
);
let mut source_positions: Vec<(usize, usize, &Node)> = sources
.iter()
.map(|n| {
let (sx, sy) = get_node_center(n);
(sx, sy, *n)
})
.collect();
source_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let (span_start, span_end) = draw_source_lines_to_merge(
&source_positions,
merge_x,
merge_y,
&coords,
canvas,
style,
direction,
Some(graph),
Some(&target.id),
);
draw_merge_line(
merge_x,
merge_y,
span_start,
span_end,
&coords,
canvas,
style,
Some(fanin_owner),
);
if span_start < span_end {
let (sx, sy) = coords.with_secondary(merge_x, merge_y, span_start);
let (ex, ey) = coords.with_secondary(merge_x, merge_y, span_end);
set_route_edge_char(canvas, sx, sy, style.corner_dl, style, Some(fanin_owner));
set_route_edge_char(canvas, ex, ey, style.corner_dr, style, Some(fanin_owner));
}
set_route_edge_char(
canvas,
merge_x,
merge_y,
style.junction_up,
style,
Some(fanin_owner),
);
let (cursor_x, cursor_y) = coords.advance(merge_x, merge_y, 1);
draw_line_primary(
cursor_x,
cursor_y,
cursor_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
if cursor_x != arrow_x {
let start_corner = if arrow_x > cursor_x {
style.corner_dl
} else {
style.corner_dr
};
set_route_edge_char(
canvas,
cursor_x,
arrow_y,
start_corner,
style,
Some(fanin_owner),
);
let (hx0, hx1) = if cursor_x < arrow_x {
(cursor_x + 1, arrow_x.saturating_sub(1))
} else {
(arrow_x + 1, cursor_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, arrow_y, style.edge_h, style, Some(fanin_owner));
}
let end_corner = if arrow_x > cursor_x {
style.corner_ur
} else {
style.corner_ul
};
set_route_edge_char(
canvas,
arrow_x,
arrow_y,
end_corner,
style,
Some(fanin_owner),
);
}
if top_y < canvas.height {
let border_fill = coords.secondary_edge_char(style);
for (sx, sy, _) in &source_positions {
let sec = coords.secondary_coord(*sx, *sy);
let (px, py) = coords.with_secondary(merge_x, top_y, sec);
if px != merge_x && px < canvas.width && py < canvas.height {
let existing = canvas.get(px, py);
if !is_textual(existing) {
canvas.set(px, py, border_fill);
}
}
}
if merge_x < canvas.width && !is_textual(canvas.get(merge_x, top_y)) {
stamp_portal_opening(canvas, merge_x, top_y, style, "merge_portal", ROUTE_Z_INDEX);
}
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(fanin_owner),
);
}
fn route_convergent_from_subgraph_lr(
sources: &[&Node],
target: &Node,
canvas: &mut Canvas,
style: &StyleChars,
sg: &crate::graph::Subgraph,
direction: Direction,
graph: &Graph,
) -> bool {
if !matches!(direction, Direction::LR | Direction::RL)
|| sources.is_empty()
|| !sg.bounds.is_valid()
{
return false;
}
let coords = OrientedCoords::new(direction);
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
let fanin_owner_id = format!("fanin:{}", target.id);
let fanin_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanin_owner_id.as_str(),
};
let mut source_positions: Vec<(usize, usize, &Node)> = sources
.iter()
.map(|n| {
let (sx, sy) = get_node_center(n);
(sx, sy, *n)
})
.collect();
source_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let span_start = source_positions
.iter()
.map(|(x, y, _)| coords.secondary_coord(*x, *y))
.min()
.unwrap_or(arrow_y);
let span_end = source_positions
.iter()
.map(|(x, y, _)| coords.secondary_coord(*x, *y))
.max()
.unwrap_or(arrow_y);
let left_border_x = sg.bounds.x;
let right_border_x = sg.bounds.x + sg.bounds.width.saturating_sub(1);
let min_inside_x = left_border_x.saturating_add(1);
let max_inside_x = right_border_x.saturating_sub(1);
if max_inside_x <= min_inside_x {
return false;
}
let merge_x = match direction {
Direction::LR => {
let max_exit_x = sources
.iter()
.map(|n| edge_exit_point(n, direction).0)
.max()
.unwrap_or(min_inside_x);
right_border_x
.saturating_sub(2)
.max(max_exit_x.saturating_add(1))
.clamp(min_inside_x, max_inside_x)
}
Direction::RL => {
let min_exit_x = sources
.iter()
.map(|n| edge_exit_point(n, direction).0)
.min()
.unwrap_or(max_inside_x);
left_border_x
.saturating_add(2)
.min(min_exit_x.saturating_sub(1))
.clamp(min_inside_x, max_inside_x)
}
_ => unreachable!(),
};
let border_x = match direction {
Direction::LR => right_border_x,
Direction::RL => left_border_x,
_ => unreachable!(),
};
let outside_x = match direction {
Direction::LR => border_x.saturating_add(1),
Direction::RL => border_x.saturating_sub(1),
_ => unreachable!(),
};
let centered_merge_y = ((span_start + span_end) / 2).clamp(
sg.bounds.y.saturating_add(1),
sg.bounds.y + sg.bounds.height.saturating_sub(2),
);
let merge_y = if centered_merge_y != arrow_y && outside_x != arrow_x {
centered_merge_y
} else {
arrow_y.clamp(
sg.bounds.y.saturating_add(1),
sg.bounds.y + sg.bounds.height.saturating_sub(2),
)
};
if std::env::var("DEBUG_FANIN").is_ok() {
eprintln!(
"horizontal fanin target={} dir={:?} sg={} span=({}, {}) arrow=({}, {}) border_x={} outside_x={} centered_merge_y={} merge_y={}",
target.id,
direction,
sg.id,
span_start,
span_end,
arrow_x,
arrow_y,
border_x,
outside_x,
centered_merge_y,
merge_y
);
}
let (actual_span_start, actual_span_end) = draw_source_lines_to_merge(
&source_positions,
merge_x,
merge_y,
&coords,
canvas,
style,
direction,
Some(graph),
Some(&target.id),
);
draw_merge_line(
merge_x,
merge_y,
actual_span_start,
actual_span_end,
&coords,
canvas,
style,
Some(fanin_owner),
);
set_route_edge_char(
canvas,
merge_x,
merge_y,
match direction {
Direction::LR => style.junction_right,
Direction::RL => style.junction_left,
_ => unreachable!(),
},
style,
Some(fanin_owner),
);
if merge_y == arrow_y {
draw_line_primary(
outside_x,
merge_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
} else {
let turn_x = match direction {
Direction::LR => arrow_x
.saturating_sub(2)
.clamp(outside_x, arrow_x.saturating_sub(1)),
Direction::RL => arrow_x
.saturating_add(2)
.clamp(arrow_x.saturating_add(1), outside_x),
_ => unreachable!(),
};
let going_before = merge_y > arrow_y;
if std::env::var("DEBUG_FANIN").is_ok() {
eprintln!(
"horizontal fanin jog target={} dir={:?} turn_x={} going_before={}",
target.id, direction, turn_x, going_before
);
}
draw_line_primary(
outside_x,
merge_y,
turn_x,
merge_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
set_route_edge_char(
canvas,
turn_x,
merge_y,
coords.corner_start_to_secondary(going_before, style),
style,
Some(fanin_owner),
);
draw_line_secondary(
turn_x,
merge_y,
turn_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
set_route_edge_char(
canvas,
turn_x,
arrow_y,
coords.corner_secondary_to_end(going_before, style),
style,
Some(fanin_owner),
);
let (seg_start_x, seg_start_y) = coords.advance(turn_x, arrow_y, 1);
draw_line_primary(
seg_start_x,
seg_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
}
for (_, sy, _) in &source_positions {
let border_y = coords.secondary_coord(border_x, *sy);
if border_y != merge_y && border_x < canvas.width && border_y < canvas.height {
let existing = canvas.get(border_x, border_y);
if !is_textual(existing) {
canvas.set(border_x, border_y, style.v);
}
}
}
if border_x < canvas.width
&& merge_y < canvas.height
&& !is_textual(canvas.get(border_x, merge_y))
{
stamp_portal_opening(
canvas,
border_x,
merge_y,
style,
"merge_portal",
ROUTE_Z_INDEX,
);
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(fanin_owner),
);
true
}
#[allow(clippy::too_many_arguments)]
fn draw_source_lines_to_merge(
source_positions: &[(usize, usize, &Node)],
merge_x: usize,
merge_y: usize,
coords: &OrientedCoords,
canvas: &mut Canvas,
style: &StyleChars,
direction: Direction,
graph: Option<&Graph>,
target_id: Option<&str>,
) -> (usize, usize) {
let mut span_start = usize::MAX;
let mut span_end = 0;
for &(src_x, src_y, _) in source_positions {
let src_secondary = coords.secondary_coord(src_x, src_y);
span_start = span_start.min(src_secondary);
span_end = span_end.max(src_secondary);
}
if matches!(direction, Direction::TD | Direction::TB) {
let merge_secondary = coords.secondary_coord(merge_x, merge_y);
span_start = span_start.min(merge_secondary);
span_end = span_end.max(merge_secondary);
}
for &(src_x, src_y, source) in source_positions {
let owner_id = graph
.zip(target_id)
.map(|(graph, target_id)| edge_route_owner_id(graph, &source.id, target_id));
let owner = owner_id.as_deref().map(|owner_id| RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: owner_id,
});
let (edge_x, edge_y) = edge_exit_point(source, direction);
let src_secondary = coords.secondary_coord(src_x, src_y);
let (merge_col_x, merge_col_y) = coords.with_secondary(merge_x, merge_y, src_secondary);
match direction {
Direction::TD | Direction::TB => {
let (start, end) = if edge_y <= merge_col_y {
(edge_y, merge_col_y)
} else {
(merge_col_y, edge_y)
};
for y in start..end {
set_route_edge_char(canvas, src_x, y, style.edge_v, style, owner);
}
}
Direction::LR => {
let (start, end) = if edge_x <= merge_col_x {
(edge_x, merge_col_x)
} else {
(merge_col_x, edge_x)
};
for x in start..end {
set_route_edge_char(canvas, x, src_y, style.edge_h, style, owner);
}
}
Direction::RL => {
let (start, end) = if merge_col_x <= edge_x {
(merge_col_x + 1, edge_x + 1)
} else {
(edge_x + 1, merge_col_x + 1)
};
for x in start..end {
set_route_edge_char(canvas, x, src_y, style.edge_h, style, owner);
}
}
Direction::BT => {
let node_border_y = source.y;
let line_start = merge_col_y.saturating_add(1);
if line_start < node_border_y {
for y in line_start..node_border_y {
set_route_edge_char(canvas, src_x, y, style.edge_v, style, owner);
}
}
}
}
match direction {
Direction::LR => {
let border_x = edge_x.saturating_sub(1);
if border_x < canvas.width && src_y < canvas.height {
set_route_edge_char(
canvas,
border_x,
src_y,
style.junction_right,
style,
owner,
);
}
}
Direction::RL => {
let border_x = edge_x.saturating_add(1);
if border_x < canvas.width && src_y < canvas.height {
set_route_edge_char(canvas, border_x, src_y, style.junction_left, style, owner);
}
}
Direction::TD | Direction::TB => {
let border_y = source.y + source.height.saturating_sub(1);
if src_x < canvas.width && border_y < canvas.height {
set_route_edge_char(canvas, src_x, border_y, style.junction_down, style, owner);
}
}
Direction::BT => {
let border_y = source.y;
if src_x < canvas.width && border_y < canvas.height {
set_route_edge_char(canvas, src_x, border_y, style.junction_up, style, owner);
}
}
}
let corner_char = get_convergence_corner(
src_secondary,
span_start,
span_end,
direction,
style,
coords,
);
set_route_edge_char(canvas, merge_col_x, merge_col_y, corner_char, style, owner);
}
(span_start, span_end)
}
fn get_convergence_corner(
src_secondary: usize,
span_start: usize,
span_end: usize,
direction: Direction,
style: &StyleChars,
coords: &OrientedCoords,
) -> char {
if src_secondary == span_start {
match direction {
Direction::TD | Direction::TB => style.corner_ul, Direction::LR => style.corner_dr, Direction::RL => style.corner_dl, Direction::BT => style.corner_dl, }
} else if src_secondary == span_end {
match direction {
Direction::TD | Direction::TB => style.corner_ur, Direction::LR => style.corner_ur, Direction::RL => style.corner_ul, Direction::BT => style.corner_dr, }
} else {
coords.junction_merge(style)
}
}
#[allow(clippy::too_many_arguments)]
fn draw_merge_line(
merge_x: usize,
merge_y: usize,
span_start: usize,
span_end: usize,
coords: &OrientedCoords,
canvas: &mut Canvas,
style: &StyleChars,
owner: Option<RouteOwner<'_>>,
) {
for pos in span_start..=span_end {
if pos == span_start || pos == span_end {
continue;
}
let (span_x, span_y) = coords.with_secondary(merge_x, merge_y, pos);
set_route_edge_char(
canvas,
span_x,
span_y,
coords.secondary_edge_char(style),
style,
owner,
);
}
}
fn set_route_char(
canvas: &mut Canvas,
x: usize,
y: usize,
ch: char,
owner: Option<RouteOwner<'_>>,
) {
if let Some(owner) = owner {
canvas.set_owned(x, y, ch, owner.kind, owner.id, ROUTE_Z_INDEX);
} else {
canvas.set(x, y, ch);
}
}
fn set_route_edge_char(
canvas: &mut Canvas,
x: usize,
y: usize,
ch: char,
style: &StyleChars,
owner: Option<RouteOwner<'_>>,
) {
if let Some(owner) = owner {
canvas.set_edge_char_owned(x, y, ch, style, owner.kind, owner.id, ROUTE_Z_INDEX);
} else {
canvas.set_edge_char(x, y, ch, style);
}
}
fn edge_route_owner_id(graph: &Graph, from_id: &str, to_id: &str) -> String {
graph
.edges
.iter()
.enumerate()
.find_map(|(idx, edge)| {
(!edge.is_back_edge && edge.from == from_id && edge.to == to_id)
.then(|| edge_owner_id(idx, edge))
})
.unwrap_or_else(|| format!("edge:?:{from_id}->{to_id}"))
}
pub fn route_convergent_edges(
from_nodes: &[&Node],
to: &Node,
canvas: &mut Canvas,
style: &StyleChars,
spacing: &SpacingConfig,
direction: Direction,
graph: &Graph,
) {
if from_nodes.is_empty() || !canvas.is_visible(to) {
return;
}
let coords = OrientedCoords::new(direction);
let fanin_owner_id = format!("fanin:{}", to.id);
let fanin_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanin_owner_id.as_str(),
};
let debug = std::env::var("TERMIFLOW_DEBUG_TIMING").is_ok();
let visible_sources: Vec<&Node> = from_nodes
.iter()
.filter(|n| canvas.is_visible(n))
.copied()
.collect();
if visible_sources.is_empty() {
return;
}
let (target_x, target_y) = get_node_center(to);
let (arrow_x, arrow_y) = adjusted_edge_entry_point(to, direction, graph);
if debug {
let ids: Vec<&str> = visible_sources.iter().map(|n| n.id.as_str()).collect();
eprintln!(
"render: convergent -> {} from {:?} merge_base=({}, {})",
to.id, ids, arrow_x, arrow_y
);
}
if matches!(direction, Direction::TD | Direction::TB) {
if let Some(source_sg_id) = visible_sources
.first()
.and_then(|n| graph.get_node_subgraph(&n.id))
{
let target_sg = graph.get_node_subgraph(&to.id);
let all_sources_same = visible_sources
.iter()
.all(|n| graph.get_node_subgraph(&n.id) == Some(source_sg_id));
if all_sources_same && target_sg != Some(source_sg_id) {
if let Some(sg) = graph.get_subgraph(source_sg_id) {
if sg.bounds.is_valid() {
route_convergent_from_subgraph_td(
&visible_sources,
to,
canvas,
style,
sg,
direction,
graph,
);
return;
}
}
}
}
} else if matches!(direction, Direction::LR | Direction::RL) {
if let Some(source_sg_id) = visible_sources
.first()
.and_then(|n| graph.get_node_subgraph(&n.id))
{
let target_sg = graph.get_node_subgraph(&to.id);
let all_sources_same = visible_sources
.iter()
.all(|n| graph.get_node_subgraph(&n.id) == Some(source_sg_id));
if all_sources_same && target_sg != Some(source_sg_id) {
if let Some(sg) = graph.get_subgraph(source_sg_id) {
if sg.bounds.is_valid()
&& route_convergent_from_subgraph_lr(
&visible_sources,
to,
canvas,
style,
sg,
direction,
graph,
)
{
return;
}
}
}
}
} else if direction == Direction::BT {
if let Some(source_sg_id) = visible_sources
.first()
.and_then(|n| graph.get_node_subgraph(&n.id))
{
let target_sg = graph.get_node_subgraph(&to.id);
let all_sources_same = visible_sources
.iter()
.all(|n| graph.get_node_subgraph(&n.id) == Some(source_sg_id));
if all_sources_same && target_sg != Some(source_sg_id) {
if let Some(sg) = graph.get_subgraph(source_sg_id) {
if sg.bounds.is_valid() {
route_convergent_from_subgraph_bt(
&visible_sources,
to,
canvas,
style,
sg,
direction,
graph,
);
return;
}
}
}
}
}
let merge_distance = match direction {
Direction::LR | Direction::RL => spacing.stem_length_horizontal,
_ => spacing.stem_length_vertical,
};
let (mut merge_x, mut merge_y) = coords.retreat(arrow_x, arrow_y, merge_distance);
let mut min_exit = usize::MAX;
let mut max_exit = 0usize;
for src in &visible_sources {
let (ex, ey) = edge_exit_point(src, direction);
let primary = coords.primary_coord(ex, ey);
min_exit = min_exit.min(primary);
max_exit = max_exit.max(primary);
}
let mut merge_primary = coords.primary_coord(merge_x, merge_y);
let arrow_primary = coords.primary_coord(arrow_x, arrow_y);
match direction {
Direction::LR => {
let min_merge = max_exit.saturating_add(1);
let max_merge_two = arrow_primary.saturating_sub(3);
let max_merge_one = arrow_primary.saturating_sub(2);
let max_merge = if max_merge_two >= min_merge {
max_merge_two
} else {
max_merge_one
};
if min_merge > max_merge {
merge_primary = max_merge;
} else {
merge_primary = merge_primary.max(min_merge);
merge_primary = merge_primary.min(max_merge);
}
}
Direction::RL => {
let max_merge = min_exit.saturating_sub(1);
let min_merge_two = arrow_primary.saturating_add(3);
let min_merge_one = arrow_primary.saturating_add(2);
let min_merge = if max_merge >= min_merge_two {
min_merge_two
} else {
min_merge_one
};
if max_merge < min_merge {
merge_primary = max_merge;
} else {
merge_primary = merge_primary.min(max_merge);
merge_primary = merge_primary.max(min_merge);
}
}
Direction::TD | Direction::TB => {
let min_merge = max_exit.saturating_add(2);
let limit = arrow_primary.saturating_sub(1);
merge_primary = min_merge.min(limit);
}
Direction::BT => {
let max_merge = min_exit.saturating_sub(2);
let limit = arrow_primary.saturating_add(2);
merge_primary = max_merge.max(limit);
}
}
coords.set_primary(&mut merge_x, &mut merge_y, merge_primary);
let mut source_positions: Vec<(usize, usize, &Node)> = visible_sources
.iter()
.map(|n| {
let (sx, sy) = get_node_center(n);
(sx, sy, *n)
})
.collect();
source_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let target_secondary = coords.secondary_coord(target_x, target_y);
let (actual_span_start, actual_span_end) = draw_source_lines_to_merge(
&source_positions,
merge_x,
merge_y,
&coords,
canvas,
style,
direction,
Some(graph),
Some(&to.id),
);
let final_span_start = actual_span_start.min(target_secondary);
let final_span_end = actual_span_end.max(target_secondary);
if matches!(direction, Direction::TD | Direction::TB) && final_span_start < final_span_end {
let (sx, sy) = coords.with_secondary(merge_x, merge_y, final_span_start);
let (ex, ey) = coords.with_secondary(merge_x, merge_y, final_span_end);
let start_char = if final_span_start == target_secondary {
if final_span_start == actual_span_start {
style.junction_right } else {
style.corner_dl }
} else {
style.corner_ul };
let end_char = if final_span_end == target_secondary {
if final_span_end == actual_span_end {
style.junction_left } else {
style.corner_dr }
} else {
style.corner_ur };
set_route_edge_char(canvas, sx, sy, start_char, style, Some(fanin_owner));
set_route_edge_char(canvas, ex, ey, end_char, style, Some(fanin_owner));
}
draw_merge_line(
merge_x,
merge_y,
final_span_start,
final_span_end,
&coords,
canvas,
style,
Some(fanin_owner),
);
let junction_char = match direction {
Direction::TD | Direction::TB => style.junction_down,
Direction::LR => style.junction_right, Direction::RL => style.junction_left, Direction::BT => style.junction_up, };
let mut merge_y_draw = merge_y;
if matches!(direction, Direction::TD | Direction::TB) {
let span_width = final_span_end.saturating_sub(final_span_start);
if span_width <= 1 {
for pos in final_span_start..=final_span_end {
let (x, y) = coords.with_secondary(merge_x, merge_y, pos);
canvas.set(x, y, ' ');
}
merge_y_draw = merge_y_draw.saturating_sub(1);
for pos in final_span_start..=final_span_end {
let (x, y) = coords.with_secondary(merge_x, merge_y_draw, pos);
canvas.set(x, y, ' ');
}
set_route_edge_char(
canvas,
merge_x,
merge_y_draw,
junction_char,
style,
Some(fanin_owner),
);
} else {
for pos in final_span_start..=final_span_end {
if pos == actual_span_start || pos == actual_span_end {
continue;
}
let (x, y) = coords.with_secondary(merge_x, merge_y, pos);
let ch = if pos == coords.secondary_coord(merge_x, merge_y) {
junction_char
} else {
coords.secondary_edge_char(style)
};
set_route_edge_char(canvas, x, y, ch, style, Some(fanin_owner));
}
}
} else {
set_route_edge_char(
canvas,
merge_x,
merge_y,
junction_char,
style,
Some(fanin_owner),
);
}
if matches!(direction, Direction::TD | Direction::TB) && final_span_start < final_span_end {
let (sx, sy) = coords.with_secondary(merge_x, merge_y_draw, final_span_start);
let (ex, ey) = coords.with_secondary(merge_x, merge_y_draw, final_span_end);
let start_char = if final_span_start == target_secondary {
if final_span_start == actual_span_start {
style.junction_right } else {
style.corner_dl }
} else {
style.corner_ul };
let end_char = if final_span_end == target_secondary {
if final_span_end == actual_span_end {
style.junction_left } else {
style.corner_dr }
} else {
style.corner_ur };
set_route_edge_char(canvas, sx, sy, start_char, style, Some(fanin_owner));
set_route_edge_char(canvas, ex, ey, end_char, style, Some(fanin_owner));
}
let (final_start_x, final_start_y) = coords.advance(merge_x, merge_y_draw, 1);
draw_line_primary(
final_start_x,
final_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
Some(fanin_owner),
);
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(fanin_owner),
);
}
fn get_node_center(node: &Node) -> (usize, usize) {
(node.center_x(), node.center_y())
}
fn edge_entry_point(node: &Node, direction: Direction) -> (usize, usize) {
match direction {
Direction::TD | Direction::TB => (node.center_x(), node.y.saturating_sub(1)),
Direction::LR => (node.x.saturating_sub(1), node.center_y()),
Direction::RL => (node.x + node.width, node.center_y()),
Direction::BT => (node.center_x(), node.bottom_y()),
}
}
fn adjusted_edge_entry_point(node: &Node, direction: Direction, graph: &Graph) -> (usize, usize) {
let default = edge_entry_point(node, direction);
if !hits_foreign_subgraph_border(node, default.0, default.1, graph) {
return default;
}
for candidate in edge_entry_candidates(node, direction) {
if !hits_foreign_subgraph_border(node, candidate.0, candidate.1, graph) {
return candidate;
}
}
default
}
fn edge_entry_candidates(node: &Node, direction: Direction) -> Vec<(usize, usize)> {
let mut candidates = Vec::new();
let push_if_new = |candidates: &mut Vec<(usize, usize)>, candidate| {
if !candidates.contains(&candidate) {
candidates.push(candidate);
}
};
match direction {
Direction::TD | Direction::TB | Direction::BT => {
let y = edge_entry_point(node, direction).1;
let center = node.center_x();
push_if_new(&mut candidates, (center, y));
let min_x = node.x.saturating_add(1);
let max_x = node.x + node.width.saturating_sub(2);
for delta in 1..=node.width {
let left = center.saturating_sub(delta);
if left >= min_x {
push_if_new(&mut candidates, (left, y));
}
let right = center.saturating_add(delta);
if right <= max_x {
push_if_new(&mut candidates, (right, y));
}
if left < min_x && right > max_x {
break;
}
}
}
Direction::LR | Direction::RL => {
let x = edge_entry_point(node, direction).0;
let center = node.center_y();
push_if_new(&mut candidates, (x, center));
let min_y = node.y.saturating_add(1);
let max_y = node.y + node.height.saturating_sub(2);
for delta in 1..=node.height {
let up = center.saturating_sub(delta);
if up >= min_y {
push_if_new(&mut candidates, (x, up));
}
let down = center.saturating_add(delta);
if down <= max_y {
push_if_new(&mut candidates, (x, down));
}
if up < min_y && down > max_y {
break;
}
}
}
}
candidates
}
fn hits_foreign_subgraph_border(node: &Node, x: usize, y: usize, graph: &Graph) -> bool {
let own_subgraph = graph.get_node_subgraph(&node.id);
graph.subgraphs.iter().any(|subgraph| {
if !subgraph.bounds.is_valid() || own_subgraph == Some(subgraph.id.as_str()) {
return false;
}
let min_x = subgraph.bounds.x;
let max_x = subgraph.bounds.x + subgraph.bounds.width.saturating_sub(1);
let min_y = subgraph.bounds.y;
let max_y = subgraph.bounds.y + subgraph.bounds.height.saturating_sub(1);
let within_x = x >= min_x && x <= max_x;
let within_y = y >= min_y && y <= max_y;
within_x && within_y && (x == min_x || x == max_x || y == min_y || y == max_y)
})
}
pub fn edge_exit_point(node: &Node, direction: Direction) -> (usize, usize) {
match direction {
Direction::TD | Direction::TB => (node.center_x(), node.bottom_y()),
Direction::LR => (node.x + node.width, node.center_y()),
Direction::RL => (node.x.saturating_sub(1), node.center_y()),
Direction::BT => (node.center_x(), node.y.saturating_sub(1)),
}
}
#[allow(clippy::too_many_arguments)]
fn draw_line_primary(
x1: usize,
y1: usize,
x2: usize,
y2: usize,
coords: &OrientedCoords,
canvas: &mut Canvas,
style: &StyleChars,
graph: Option<&Graph>,
owner: Option<RouteOwner<'_>>,
) {
let char = coords.primary_edge_char(style);
match coords.primary {
crate::orientation::Axis::Horizontal => {
let (start, end) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
for x in start..=end {
if let Some(g) = graph {
if is_subgraph_title_cell(g, x, y1) {
continue;
}
}
set_route_edge_char(canvas, x, y1, char, style, owner);
}
}
crate::orientation::Axis::Vertical => {
let (start, end) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
for y in start..=end {
if let Some(g) = graph {
if is_subgraph_title_cell(g, x1, y) {
continue;
}
}
set_route_edge_char(canvas, x1, y, char, style, owner);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn draw_line_secondary(
x1: usize,
y1: usize,
x2: usize,
y2: usize,
coords: &OrientedCoords,
canvas: &mut Canvas,
style: &StyleChars,
graph: Option<&Graph>,
owner: Option<RouteOwner<'_>>,
) {
let char = coords.secondary_edge_char(style);
match coords.secondary {
crate::orientation::Axis::Horizontal => {
let (start, end) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
for x in start..=end {
if x != x1 && x != x2 {
if let Some(g) = graph {
if is_subgraph_title_cell(g, x, y1) {
continue;
}
}
set_route_edge_char(canvas, x, y1, char, style, owner);
}
}
}
crate::orientation::Axis::Vertical => {
let (start, end) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
for y in start..=end {
if y != y1 && y != y2 {
if let Some(g) = graph {
if is_subgraph_title_cell(g, x1, y) {
continue;
}
}
set_route_edge_char(canvas, x1, y, char, style, owner);
}
}
}
}
}
fn is_subgraph_title_cell(graph: &Graph, x: usize, y: usize) -> bool {
graph.subgraphs.iter().any(|sg| {
if !sg.has_title() || !sg.bounds.is_valid() {
return false;
}
let title_y = subgraph_title_y(&sg.bounds, graph.direction);
let Some(title) = sg.title.as_deref() else {
return false;
};
let Some((start_x, end_x)) = title_span(&sg.bounds, title, graph.direction) else {
return false;
};
y == title_y && x >= start_x && x <= end_x
})
}
fn preferred_portal_x(
bounds: &crate::graph::Rectangle,
title: Option<&str>,
desired: usize,
canvas: &Canvas,
direction: Direction,
avoid_title: bool,
) -> usize {
let min = bounds.x.saturating_add(1);
let max = bounds.x + bounds.width.saturating_sub(2);
let _ = canvas;
let mut x = desired.clamp(min, max);
let mut protected_title_span: Option<(usize, usize)> = None;
if avoid_title {
if let Some(t) = title {
let Some((start, end)) = title_span(bounds, t, direction) else {
return x;
};
protected_title_span = Some((start, end));
let protected_start = start.saturating_sub(2);
let protected_end = end.saturating_add(2).min(max);
if x >= protected_start && x <= protected_end {
if direction == Direction::BT {
let left = (protected_start > min).then(|| protected_start.saturating_sub(1));
let right = (protected_end < max).then(|| protected_end + 1);
x = match (left, right) {
(Some(left), Some(right)) => {
let left_distance = x.abs_diff(left);
let right_distance = x.abs_diff(right);
if left_distance < right_distance {
left
} else if right_distance < left_distance {
right
} else if x <= (protected_start + protected_end) / 2 {
left
} else {
right
}
}
(Some(left), None) => left,
(None, Some(right)) => right,
(None, None) => x,
};
} else if protected_end < max {
x = protected_end + 1;
} else if protected_start > min {
x = protected_start.saturating_sub(1);
}
}
}
}
if direction == Direction::BT {
if let Some((s, e)) = protected_title_span {
let in_title_text = |pos: usize| pos >= s && pos <= e;
if x == min {
let candidate = min.saturating_add(1);
if candidate <= max && !in_title_text(candidate) {
x = candidate;
}
} else if x == max {
let candidate = max.saturating_sub(1);
if candidate >= min && !in_title_text(candidate) {
x = candidate;
}
}
}
}
x
}
fn nearest_title_safe_x(
bounds: &crate::graph::Rectangle,
title: Option<&str>,
desired: usize,
direction: Direction,
) -> usize {
let min = bounds.x.saturating_add(1);
let max = bounds.x + bounds.width.saturating_sub(2);
let x = desired.clamp(min, max);
let Some(title) = title else {
return x;
};
let Some((start, end)) = title_span(bounds, title, direction) else {
return x;
};
let protected_start = start.saturating_sub(2);
let protected_end = end.saturating_add(2).min(max);
if x < protected_start || x > protected_end {
return x;
}
let left = (protected_start > min).then(|| protected_start.saturating_sub(1));
let right = (protected_end < max).then(|| protected_end + 1);
match (left, right) {
(Some(left), Some(right)) => {
let left_distance = x.abs_diff(left);
let right_distance = x.abs_diff(right);
if left_distance < right_distance {
left
} else if right_distance < left_distance {
right
} else if x <= (protected_start + protected_end) / 2 {
left
} else {
right
}
}
(Some(left), None) => left,
(None, Some(right)) => right,
(None, None) => x,
}
}
fn bounds_contains_subgraph(
outer: &crate::graph::Rectangle,
inner: &crate::graph::Rectangle,
) -> bool {
outer.is_valid()
&& inner.is_valid()
&& inner.x >= outer.x
&& inner.y >= outer.y
&& inner.x + inner.width <= outer.x + outer.width
&& inner.y + inner.height <= outer.y + outer.height
}
fn bounds_contains_node(bounds: &crate::graph::Rectangle, node: &Node) -> bool {
let node_right = node.x + node.width;
let node_bottom = node.y + node.height.max(crate::style::BOX_HEIGHT);
bounds.is_valid()
&& node.x >= bounds.x
&& node.y >= bounds.y
&& node_right <= bounds.x + bounds.width
&& node_bottom <= bounds.y + bounds.height
}
fn has_visual_container_for_nested_entry(
graph: &Graph,
source: &Node,
target_sg: &crate::graph::Subgraph,
) -> bool {
graph.subgraphs.iter().any(|candidate| {
candidate.id != target_sg.id
&& bounds_contains_subgraph(&candidate.bounds, &target_sg.bounds)
&& (graph.is_node_in_subgraph_tree(&source.id, &candidate.id)
|| bounds_contains_node(&candidate.bounds, source))
})
}
fn smallest_visual_container<'a>(
graph: &'a Graph,
inner: &crate::graph::Subgraph,
target: &Node,
) -> Option<&'a crate::graph::Subgraph> {
graph
.subgraphs
.iter()
.filter(|candidate| {
candidate.id != inner.id
&& bounds_contains_subgraph(&candidate.bounds, &inner.bounds)
&& !bounds_contains_node(&candidate.bounds, target)
})
.min_by_key(|candidate| candidate.bounds.width * candidate.bounds.height)
}
fn td_title_safe_entry_y(subgraph: &crate::graph::Subgraph) -> usize {
let min_inside = subgraph.bounds.y.saturating_add(1);
let max_inside = subgraph
.bounds
.y
.saturating_add(subgraph.bounds.height.saturating_sub(2));
let desired = if subgraph.has_title() {
subgraph.bounds.y.saturating_add(3)
} else {
min_inside
};
desired.clamp(min_inside, max_inside)
}
#[allow(dead_code, clippy::too_many_arguments)]
fn route_cross_subgraph_td(
from: &Node,
to: &Node,
stem_start_x: usize,
stem_start_y: usize,
arrow_x: usize,
arrow_y: usize,
canvas: &mut Canvas,
style: &StyleChars,
graph: &Graph,
owner: Option<RouteOwner<'_>>,
) -> bool {
let debug_timing = std::env::var("TERMIFLOW_DEBUG_TIMING").is_ok();
let from_sg = graph.get_node_subgraph(&from.id);
let to_sg = graph.get_node_subgraph(&to.id);
if from_sg == to_sg {
return false;
}
let Some(sg_id) = to_sg else {
return false;
};
let Some(sg) = graph.get_subgraph(sg_id) else {
return false;
};
if !sg.bounds.is_valid() {
return false;
}
let entering_from_above =
stem_start_y < sg.bounds.y && arrow_y >= sg.bounds.y.saturating_add(1);
if entering_from_above {
let (_, enter_subgraphs) = graph.edge_boundary_crossings(&from.id, &to.id);
let mut current_x = stem_start_x;
let mut current_y = stem_start_y;
let final_entry_x = preferred_portal_x(
&sg.bounds,
sg.title.as_deref(),
arrow_x,
canvas,
graph.direction,
true,
);
let shared_entry_x = enter_subgraphs
.iter()
.rev()
.filter_map(|ancestor_id| graph.get_subgraph(ancestor_id))
.filter(|ancestor_sg| ancestor_sg.bounds.is_valid())
.fold(final_entry_x, |entry_x, ancestor_sg| {
nearest_title_safe_x(
&ancestor_sg.bounds,
ancestor_sg.title.as_deref(),
entry_x,
graph.direction,
)
});
for ancestor_id in enter_subgraphs.iter().rev() {
let Some(ancestor_sg) = graph.get_subgraph(ancestor_id) else {
continue;
};
if !ancestor_sg.bounds.is_valid() {
continue;
}
let outside_y = ancestor_sg.bounds.y.saturating_sub(1);
if current_y <= outside_y {
for y in current_y..=outside_y {
set_route_edge_char(canvas, current_x, y, style.edge_v, style, owner);
}
}
let entry_x = nearest_title_safe_x(
&ancestor_sg.bounds,
ancestor_sg.title.as_deref(),
shared_entry_x,
graph.direction,
);
if entry_x != current_x && outside_y < canvas.height {
let start_corner = if entry_x > current_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(canvas, current_x, outside_y, start_corner, style, owner);
let (hx0, hx1) = if entry_x > current_x {
(current_x + 1, entry_x.saturating_sub(1))
} else {
(entry_x + 1, current_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, outside_y, style.edge_h, style, owner);
}
let end_corner = if entry_x > current_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(canvas, entry_x, outside_y, end_corner, style, owner);
}
current_x = entry_x;
current_y = ancestor_sg.bounds.y.saturating_add(1).min(
ancestor_sg
.bounds
.y
.saturating_add(ancestor_sg.bounds.height.saturating_sub(2)),
);
}
let mut bridge_y = td_title_safe_entry_y(sg).max(current_y).min(arrow_y);
if current_x != arrow_x && arrow_y > current_y {
bridge_y = bridge_y.min(arrow_y.saturating_sub(1)).max(current_y);
}
if bridge_y >= current_y && current_y < canvas.height {
for y in current_y..=bridge_y {
set_route_edge_char(canvas, current_x, y, style.edge_v, style, owner);
}
}
if current_x != arrow_x {
let start_corner = if arrow_x > current_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(canvas, current_x, bridge_y, start_corner, style, owner);
let (hx0, hx1) = if arrow_x > current_x {
(current_x.saturating_add(1), arrow_x.saturating_sub(1))
} else {
(arrow_x.saturating_add(1), current_x.saturating_sub(1))
};
for x in hx0..=hx1 {
if is_subgraph_title_cell(graph, x, bridge_y) {
continue;
}
set_route_edge_char(canvas, x, bridge_y, style.edge_h, style, owner);
}
let end_corner = if arrow_x > current_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(canvas, arrow_x, bridge_y, end_corner, style, owner);
}
if arrow_y > bridge_y && arrow_x < canvas.width {
for y in bridge_y.saturating_add(1)..=arrow_y {
if is_subgraph_title_cell(graph, arrow_x, y) {
continue;
}
set_route_edge_char(canvas, arrow_x, y, style.edge_v, style, owner);
}
}
if debug_timing {
eprintln!(
" cross-subgraph enter-under-title {} -> {} portal_x={} bridge_y={} border_y={}",
from.id, to.id, current_x, bridge_y, sg.bounds.y
);
}
return true;
}
let target_left_border = sg.bounds.x;
let target_right_border = sg.bounds.x + sg.bounds.width.saturating_sub(1);
let target_top_interior = sg.bounds.y.saturating_add(1);
let target_bottom_interior = sg.bounds.y + sg.bounds.height.saturating_sub(2);
let has_visual_container = has_visual_container_for_nested_entry(graph, from, sg);
let can_side_enter = has_visual_container
&& stem_start_y >= target_top_interior
&& stem_start_y <= target_bottom_interior;
if can_side_enter && stem_start_x < target_left_border {
let entry_y = stem_start_y.clamp(target_top_interior, target_bottom_interior);
set_route_edge_char(
canvas,
stem_start_x,
stem_start_y,
style.corner_ul,
style,
owner,
);
for x in (stem_start_x + 1)..target_left_border {
set_route_edge_char(canvas, x, entry_y, style.edge_h, style, owner);
}
stamp_portal_opening(
canvas,
target_left_border,
entry_y,
style,
"side_entry_portal",
ROUTE_Z_INDEX,
);
let turn_x = arrow_x.clamp(
sg.bounds.x.saturating_add(1),
sg.bounds.x + sg.bounds.width.saturating_sub(2),
);
let inside_start = target_left_border.saturating_add(1);
if turn_x >= inside_start {
let start_corner = style.corner_dr;
set_route_edge_char(canvas, inside_start, entry_y, start_corner, style, owner);
for x in (inside_start + 1)..turn_x {
set_route_edge_char(canvas, x, entry_y, style.edge_h, style, owner);
}
}
if turn_x != inside_start {
set_route_edge_char(canvas, turn_x, entry_y, style.corner_dl, style, owner);
}
let (vy0, vy1) = if entry_y < arrow_y {
(entry_y.saturating_add(1), arrow_y)
} else {
(arrow_y, entry_y.saturating_sub(1))
};
for y in vy0..=vy1 {
if is_subgraph_title_cell(graph, turn_x, y) {
continue;
}
set_route_edge_char(canvas, turn_x, y, style.edge_v, style, owner);
}
if turn_x != arrow_x {
let corner = if turn_x < arrow_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(canvas, turn_x, arrow_y, corner, style, owner);
let (hx0, hx1) = if turn_x < arrow_x {
(turn_x + 1, arrow_x)
} else {
(arrow_x, turn_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, arrow_y, style.edge_h, style, owner);
}
}
return true;
}
if can_side_enter && stem_start_x > target_right_border {
let entry_y = stem_start_y.clamp(target_top_interior, target_bottom_interior);
set_route_edge_char(
canvas,
stem_start_x,
stem_start_y,
style.corner_ur,
style,
owner,
);
for x in (target_right_border + 1)..stem_start_x {
set_route_edge_char(canvas, x, entry_y, style.edge_h, style, owner);
}
stamp_portal_opening(
canvas,
target_right_border,
entry_y,
style,
"side_entry_portal",
ROUTE_Z_INDEX,
);
let turn_x = arrow_x.clamp(
sg.bounds.x.saturating_add(1),
sg.bounds.x + sg.bounds.width.saturating_sub(2),
);
let inside_start = target_right_border.saturating_sub(1);
if turn_x <= inside_start {
let start_corner = style.corner_dl;
set_route_edge_char(canvas, inside_start, entry_y, start_corner, style, owner);
for x in (turn_x + 1)..inside_start {
set_route_edge_char(canvas, x, entry_y, style.edge_h, style, owner);
}
}
if turn_x != inside_start {
set_route_edge_char(canvas, turn_x, entry_y, style.corner_dr, style, owner);
}
let (vy0, vy1) = if entry_y < arrow_y {
(entry_y.saturating_add(1), arrow_y)
} else {
(arrow_y, entry_y.saturating_sub(1))
};
for y in vy0..=vy1 {
if is_subgraph_title_cell(graph, turn_x, y) {
continue;
}
set_route_edge_char(canvas, turn_x, y, style.edge_v, style, owner);
}
if turn_x != arrow_x {
let corner = if turn_x < arrow_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(canvas, turn_x, arrow_y, corner, style, owner);
let (hx0, hx1) = if turn_x < arrow_x {
(turn_x + 1, arrow_x)
} else {
(arrow_x, turn_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, arrow_y, style.edge_h, style, owner);
}
}
return true;
}
let mut portal_x = preferred_portal_x(
&sg.bounds,
sg.title.as_deref(),
arrow_x,
canvas,
graph.direction,
false,
);
let cursor_x = stem_start_x;
let mut cursor_y = stem_start_y;
let mut walked_to_source_border = false;
if let Some(src_id) = from_sg {
if let Some(src_sg) = graph.get_subgraph(src_id) {
let src_border_y = src_sg
.bounds
.y
.saturating_add(src_sg.bounds.height.saturating_sub(1));
let exit_y = src_border_y.min(arrow_y);
walked_to_source_border = exit_y == src_border_y;
for y in cursor_y..=exit_y {
if is_subgraph_title_cell(graph, cursor_x, y) {
continue;
}
set_route_edge_char(canvas, cursor_x, y, style.edge_v, style, owner);
}
cursor_y = exit_y;
portal_x = preferred_portal_x(
&sg.bounds,
sg.title.as_deref(),
arrow_x,
canvas,
graph.direction,
true,
);
}
}
let portal_y = arrow_y
.saturating_sub(1)
.max(td_title_safe_entry_y(sg))
.max(cursor_y.saturating_add(1))
.min(arrow_y);
if debug_timing {
eprintln!(
" cross-subgraph {:?}->{:?} via portal ({}, {}) from ({}, {})",
from.id, to.id, portal_x, portal_y, stem_start_x, stem_start_y
);
}
if portal_x != cursor_x {
let start_corner = if portal_x > cursor_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(canvas, cursor_x, cursor_y, start_corner, style, owner);
let (hx0, hx1) = if portal_x > cursor_x {
(cursor_x + 1, portal_x.saturating_sub(1))
} else {
(portal_x + 1, cursor_x.saturating_sub(1))
};
for x in hx0..=hx1 {
if is_subgraph_title_cell(graph, x, cursor_y) {
continue;
}
set_route_edge_char(canvas, x, cursor_y, style.edge_h, style, owner);
}
let end_corner = if portal_x > cursor_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(canvas, portal_x, cursor_y, end_corner, style, owner);
}
if portal_y > cursor_y {
let start_y = if portal_x == cursor_x {
cursor_y
} else {
cursor_y.saturating_add(1)
};
for y in start_y..=portal_y {
if is_subgraph_title_cell(graph, portal_x, y) {
continue;
}
set_route_edge_char(canvas, portal_x, y, style.edge_v, style, owner);
}
}
if portal_x != arrow_x {
let corner = if portal_x < arrow_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(canvas, portal_x, arrow_y, corner, style, owner);
let (hx0, hx1) = if portal_x < arrow_x {
(portal_x + 1, arrow_x)
} else {
(arrow_x, portal_x.saturating_sub(1))
};
for x in hx0..=hx1 {
if is_subgraph_title_cell(graph, x, arrow_y) {
continue;
}
set_route_edge_char(canvas, x, arrow_y, style.edge_h, style, owner);
}
} else if arrow_y > portal_y {
for y in (portal_y + 1)..=arrow_y {
if is_subgraph_title_cell(graph, portal_x, y) {
continue;
}
set_route_edge_char(canvas, portal_x, y, style.edge_v, style, owner);
}
}
if walked_to_source_border {
let Some(src_sg_id) = from_sg else {
return true;
};
if let Some(src_sg) = graph.get_subgraph(src_sg_id) {
let border_y = src_sg.bounds.y + src_sg.bounds.height.saturating_sub(1);
if portal_x < canvas.width && border_y < canvas.height {
set_route_edge_char(canvas, cursor_x, border_y, style.edge_v, style, owner);
}
}
}
let tgt_border_y = sg.bounds.y;
if !sg.has_title()
&& portal_x < canvas.width
&& tgt_border_y < canvas.height
&& !is_textual(canvas.get(portal_x, tgt_border_y))
{
set_route_edge_char(canvas, portal_x, tgt_border_y, style.edge_v, style, owner);
}
true
}
#[allow(clippy::too_many_arguments)]
fn route_cross_subgraph_bt(
from: &Node,
to: &Node,
stem_start_x: usize,
stem_start_y: usize,
arrow_x: usize,
arrow_y: usize,
canvas: &mut Canvas,
style: &StyleChars,
graph: &Graph,
owner: Option<RouteOwner<'_>>,
) -> bool {
let coords = OrientedCoords::new(Direction::BT);
let from_sg = graph.get_node_subgraph(&from.id);
let to_sg = graph.get_node_subgraph(&to.id);
if from_sg == to_sg {
return false;
}
if let Some(tgt_id) = to_sg {
let Some(tgt_sg) = graph.get_subgraph(tgt_id) else {
return false;
};
if tgt_sg.bounds.is_valid() {
let tgt_border_y = tgt_sg.bounds.y + tgt_sg.bounds.height.saturating_sub(1);
let entering_from_below = stem_start_y > tgt_border_y && arrow_y < tgt_border_y;
if entering_from_below {
let (_, enter_subgraphs) = graph.edge_boundary_crossings(&from.id, &to.id);
if enter_subgraphs.len() <= 1 {
let entry_x = preferred_portal_x(
&tgt_sg.bounds,
tgt_sg.title.as_deref(),
arrow_x,
canvas,
Direction::BT,
true,
);
let outside_y = tgt_border_y.saturating_add(1);
let inside_y = tgt_border_y.saturating_sub(1);
draw_line_primary(
stem_start_x,
stem_start_y,
stem_start_x,
outside_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
if entry_x != stem_start_x && outside_y < canvas.height {
let start_corner = if entry_x > stem_start_x {
style.corner_dl
} else {
style.corner_dr
};
set_route_edge_char(
canvas,
stem_start_x,
outside_y,
start_corner,
style,
owner,
);
let (hx0, hx1) = if entry_x > stem_start_x {
(stem_start_x + 1, entry_x.saturating_sub(1))
} else {
(entry_x + 1, stem_start_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, outside_y, style.edge_h, style, owner);
}
let end_corner = if entry_x > stem_start_x {
style.corner_ur
} else {
style.corner_ul
};
set_route_edge_char(canvas, entry_x, outside_y, end_corner, style, owner);
}
if tgt_border_y < canvas.height {
set_route_edge_char(
canvas,
entry_x,
tgt_border_y,
style.edge_v,
style,
owner,
);
}
if entry_x != arrow_x && inside_y < canvas.height {
let start_corner = if arrow_x > entry_x {
style.corner_dl
} else {
style.corner_dr
};
set_route_edge_char(canvas, entry_x, inside_y, start_corner, style, owner);
let (hx0, hx1) = if arrow_x > entry_x {
(entry_x + 1, arrow_x.saturating_sub(1))
} else {
(arrow_x + 1, entry_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, inside_y, style.edge_h, style, owner);
}
let end_corner = if arrow_x > entry_x {
style.corner_ur
} else {
style.corner_ul
};
set_route_edge_char(canvas, arrow_x, inside_y, end_corner, style, owner);
if arrow_y < inside_y {
draw_line_primary(
arrow_x,
inside_y.saturating_sub(1),
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
}
} else {
draw_line_primary(
entry_x,
inside_y,
entry_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
}
} else {
let mut current_x = stem_start_x;
let mut current_y = stem_start_y;
for ancestor_id in enter_subgraphs.iter().rev() {
let Some(ancestor_sg) = graph.get_subgraph(ancestor_id) else {
continue;
};
if !ancestor_sg.bounds.is_valid() {
continue;
}
let border_y = ancestor_sg
.bounds
.y
.saturating_add(ancestor_sg.bounds.height.saturating_sub(1));
let outside_y = border_y.saturating_add(1);
draw_line_primary(
current_x,
current_y,
current_x,
outside_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
let entry_x = if *ancestor_id == tgt_id {
preferred_portal_x(
&ancestor_sg.bounds,
ancestor_sg.title.as_deref(),
arrow_x,
canvas,
Direction::BT,
true,
)
} else {
nearest_title_safe_x(
&ancestor_sg.bounds,
ancestor_sg.title.as_deref(),
current_x,
Direction::BT,
)
};
if entry_x != current_x && outside_y < canvas.height {
let start_corner = if entry_x > current_x {
style.corner_dl
} else {
style.corner_dr
};
set_route_edge_char(
canvas,
current_x,
outside_y,
start_corner,
style,
owner,
);
let (hx0, hx1) = if entry_x > current_x {
(current_x + 1, entry_x.saturating_sub(1))
} else {
(entry_x + 1, current_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(
canvas,
x,
outside_y,
style.edge_h,
style,
owner,
);
}
let end_corner = if entry_x > current_x {
style.corner_ur
} else {
style.corner_ul
};
set_route_edge_char(
canvas, entry_x, outside_y, end_corner, style, owner,
);
}
current_x = entry_x;
current_y = border_y.saturating_sub(1);
}
draw_line_primary(
current_x,
current_y,
current_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
}
return true;
}
}
}
let Some(src_id) = from_sg else {
return false;
};
let Some(src_sg) = graph.get_subgraph(src_id) else {
return false;
};
if !src_sg.bounds.is_valid() {
return false;
}
let border_y = src_sg.bounds.y;
let max_inside_y = border_y + src_sg.bounds.height.saturating_sub(2);
let inside_y = border_y.saturating_add(1).min(max_inside_y);
let portal_x = preferred_portal_x(
&src_sg.bounds,
src_sg.title.as_deref(),
stem_start_x,
canvas,
Direction::BT,
false,
);
draw_line_primary(
stem_start_x,
stem_start_y,
stem_start_x,
inside_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
if portal_x != stem_start_x {
let start_corner = if portal_x > stem_start_x {
style.corner_dl } else {
style.corner_dr };
set_route_edge_char(canvas, stem_start_x, inside_y, start_corner, style, owner);
let (hx0, hx1) = if portal_x > stem_start_x {
(stem_start_x + 1, portal_x.saturating_sub(1))
} else {
(portal_x + 1, stem_start_x.saturating_sub(1))
};
for x in hx0..=hx1 {
if is_subgraph_title_cell(graph, x, inside_y) {
continue;
}
set_route_edge_char(canvas, x, inside_y, style.edge_h, style, owner);
}
let end_corner = if portal_x > stem_start_x {
style.corner_ur } else {
style.corner_ul };
set_route_edge_char(canvas, portal_x, inside_y, end_corner, style, owner);
}
let border_row_y = border_y;
let outside_y = border_y.saturating_sub(1);
let bridge_on_border_row = portal_x != arrow_x;
if inside_y > border_row_y {
draw_line_primary(
portal_x,
inside_y.saturating_sub(1),
portal_x,
border_row_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
}
if !bridge_on_border_row {
draw_line_primary(
portal_x,
border_row_y,
portal_x,
outside_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
}
if bridge_on_border_row {
let start_corner = if arrow_x > portal_x {
style.corner_dl
} else {
style.corner_dr
};
set_route_edge_char(canvas, portal_x, border_row_y, start_corner, style, owner);
let (hx0, hx1) = if arrow_x > portal_x {
(portal_x + 1, arrow_x.saturating_sub(1))
} else {
(arrow_x + 1, portal_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, border_row_y, style.edge_h, style, owner);
}
let end_corner = if arrow_x > portal_x {
style.corner_ur
} else {
style.corner_ul
};
set_route_edge_char(canvas, arrow_x, border_row_y, end_corner, style, owner);
if arrow_y < border_row_y {
draw_line_primary(
arrow_x,
border_row_y.saturating_sub(1),
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
}
} else if portal_x != arrow_x && border_y > 0 {
let start_corner = if arrow_x > portal_x {
style.corner_dl } else {
style.corner_dr };
set_route_edge_char(canvas, portal_x, outside_y, start_corner, style, owner);
let (hx0, hx1) = if arrow_x > portal_x {
(portal_x + 1, arrow_x.saturating_sub(1))
} else {
(arrow_x + 1, portal_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, outside_y, style.edge_h, style, owner);
}
let end_corner = if arrow_x > portal_x {
style.corner_ur } else {
style.corner_ul };
set_route_edge_char(canvas, arrow_x, outside_y, end_corner, style, owner);
let v_start_y = outside_y.saturating_sub(1);
draw_line_primary(
arrow_x,
v_start_y,
arrow_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
} else if !bridge_on_border_row {
draw_line_primary(
portal_x,
outside_y,
portal_x,
arrow_y,
&coords,
canvas,
style,
Some(graph),
owner,
);
if portal_x != arrow_x {
let corner = if portal_x < arrow_x {
style.corner_dl } else {
style.corner_dr };
set_route_edge_char(canvas, portal_x, arrow_y, corner, style, owner);
let (hx0, hx1) = if portal_x < arrow_x {
(portal_x + 1, arrow_x)
} else {
(arrow_x, portal_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, arrow_y, style.edge_h, style, owner);
}
}
}
if portal_x < canvas.width
&& border_y < canvas.height
&& !is_textual(canvas.get(portal_x, border_y))
&& !bridge_on_border_row
{
set_route_char(canvas, portal_x, border_y, style.edge_v, owner);
}
true
}
fn route_divergent_into_subgraph_td(
source: &Node,
targets: &[&Node],
canvas: &mut Canvas,
style: &StyleChars,
sg: &crate::graph::Subgraph,
direction: Direction,
graph: &Graph,
) {
if targets.is_empty() || !sg.bounds.is_valid() {
return;
}
let coords = OrientedCoords::new(direction);
let fanout_owner_id = format!("fanout:{}", source.id);
let fanout_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanout_owner_id.as_str(),
};
let mut target_positions: Vec<(usize, usize, &Node)> = targets
.iter()
.map(|n| {
let (tx, ty) = get_node_center(n);
(tx, ty, *n)
})
.collect();
target_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let border_y = sg.bounds.y;
let outside_y = border_y.saturating_sub(1);
let entry_y = border_y.saturating_add(1);
let min_inner_x = sg.bounds.x.saturating_add(1);
let max_inner_x = sg.bounds.x + sg.bounds.width.saturating_sub(2);
let (stem_x, stem_y) = edge_exit_point(source, direction);
let entry_x = stem_x.clamp(min_inner_x, max_inner_x);
set_route_edge_char(
canvas,
stem_x,
stem_y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
let turn_y = if stem_y < outside_y {
outside_y
} else {
stem_y
};
if stem_y < outside_y {
for y in (stem_y + 1)..=outside_y {
set_route_edge_char(
canvas,
stem_x,
y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
}
if entry_x != stem_x && turn_y < canvas.height {
let start_corner = if entry_x > stem_x {
style.corner_ul
} else {
style.corner_ur
};
set_route_edge_char(
canvas,
stem_x,
turn_y,
start_corner,
style,
Some(fanout_owner),
);
let (hx0, hx1) = if entry_x > stem_x {
(stem_x.saturating_add(1), entry_x.saturating_sub(1))
} else {
(entry_x.saturating_add(1), stem_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(canvas, x, turn_y, style.edge_h, style, Some(fanout_owner));
}
let end_corner = if entry_x > stem_x {
style.corner_dr
} else {
style.corner_dl
};
set_route_edge_char(
canvas,
entry_x,
turn_y,
end_corner,
style,
Some(fanout_owner),
);
}
let min_x = target_positions
.iter()
.map(|(x, _, _)| *x)
.min()
.unwrap_or(entry_x)
.min(entry_x);
let max_x = target_positions
.iter()
.map(|(x, _, _)| *x)
.max()
.unwrap_or(entry_x)
.max(entry_x);
let min_arrow_y = targets
.iter()
.map(|n| adjusted_edge_entry_point(n, direction, graph).1)
.min()
.unwrap_or(entry_y + 3);
let spine_y = entry_y;
if spine_y < canvas.height {
for (tx, _, _) in &target_positions {
if *tx < canvas.width {
canvas.set(*tx, spine_y, ' ');
}
}
set_route_edge_char(
canvas,
entry_x,
spine_y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
let mut branch_y = spine_y.saturating_add(1);
if branch_y + 1 >= min_arrow_y {
branch_y = min_arrow_y.saturating_sub(2);
}
branch_y = branch_y.max(spine_y.saturating_add(1));
if branch_y > spine_y.saturating_add(1) {
for y in (spine_y + 1)..branch_y {
if entry_x < canvas.width && y < canvas.height {
set_route_edge_char(
canvas,
entry_x,
y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
}
}
for x in min_x..=max_x {
set_route_edge_char(canvas, x, branch_y, style.edge_h, style, Some(fanout_owner));
}
set_route_char(canvas, min_x, branch_y, style.corner_dl, Some(fanout_owner));
set_route_char(canvas, max_x, branch_y, style.corner_dr, Some(fanout_owner));
let entry_has_drop = target_positions.iter().any(|(tx, _, _)| *tx == entry_x);
let entry_char = if min_x == max_x {
style.edge_v
} else if entry_x == min_x {
if entry_has_drop {
style.junction_right
} else {
style.corner_dl
}
} else if entry_x == max_x {
if entry_has_drop {
style.junction_left
} else {
style.corner_dr
}
} else if entry_has_drop {
style.cross
} else {
style.junction_up
};
set_route_char(canvas, entry_x, branch_y, entry_char, Some(fanout_owner));
for (tx, _, target) in target_positions {
let branch_owner_id = edge_route_owner_id(graph, &source.id, &target.id);
let branch_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: branch_owner_id.as_str(),
};
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, direction, graph);
let start_y = branch_y.saturating_add(1);
for y in start_y..arrow_y {
set_route_edge_char(canvas, tx, y, style.edge_v, style, Some(branch_owner));
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(branch_owner),
);
}
}
fn route_divergent_into_subgraph_bt(
source: &Node,
targets: &[&Node],
canvas: &mut Canvas,
style: &StyleChars,
sg: &crate::graph::Subgraph,
graph: &Graph,
) {
if targets.is_empty() || !sg.bounds.is_valid() {
return;
}
let coords = OrientedCoords::new(Direction::BT);
let fanout_owner_id = format!("fanout:{}", source.id);
let fanout_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: fanout_owner_id.as_str(),
};
let mut target_positions: Vec<(usize, usize, &Node)> = targets
.iter()
.map(|n| {
let (tx, ty) = get_node_center(n);
(tx, ty, *n)
})
.collect();
target_positions.sort_by_key(|(x, y, _)| coords.secondary_coord(*x, *y));
let border_y = sg.bounds.y + sg.bounds.height.saturating_sub(1);
let outside_y = border_y.saturating_add(1);
let entry_y = border_y.saturating_sub(1);
let (stem_x, stem_y) = edge_exit_point(source, Direction::BT);
let entry_x = preferred_portal_x(
&sg.bounds,
sg.title.as_deref(),
stem_x,
canvas,
Direction::BT,
true,
);
set_route_edge_char(
canvas,
stem_x,
stem_y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
if stem_y > outside_y {
draw_line_primary(
stem_x,
stem_y,
stem_x,
outside_y,
&coords,
canvas,
style,
Some(graph),
Some(fanout_owner),
);
}
if entry_x != stem_x && outside_y < canvas.height {
let start_corner = if entry_x > stem_x {
style.corner_dl
} else {
style.corner_dr
};
set_route_edge_char(
canvas,
stem_x,
outside_y,
start_corner,
style,
Some(fanout_owner),
);
let (hx0, hx1) = if entry_x > stem_x {
(stem_x.saturating_add(1), entry_x.saturating_sub(1))
} else {
(entry_x.saturating_add(1), stem_x.saturating_sub(1))
};
for x in hx0..=hx1 {
set_route_edge_char(
canvas,
x,
outside_y,
style.edge_h,
style,
Some(fanout_owner),
);
}
let end_corner = if entry_x > stem_x {
style.corner_ur
} else {
style.corner_ul
};
set_route_edge_char(
canvas,
entry_x,
outside_y,
end_corner,
style,
Some(fanout_owner),
);
}
if entry_y < canvas.height {
set_route_edge_char(
canvas,
entry_x,
entry_y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
let min_x = target_positions
.iter()
.map(|(x, _, _)| *x)
.min()
.unwrap_or(entry_x)
.min(entry_x);
let max_x = target_positions
.iter()
.map(|(x, _, _)| *x)
.max()
.unwrap_or(entry_x)
.max(entry_x);
let max_arrow_y = targets
.iter()
.map(|n| adjusted_edge_entry_point(n, Direction::BT, graph).1)
.max()
.unwrap_or(entry_y.saturating_sub(3));
let mut branch_y = entry_y.saturating_sub(1);
if branch_y <= max_arrow_y {
branch_y = max_arrow_y.saturating_add(1);
}
branch_y = branch_y.min(entry_y.saturating_sub(1));
if branch_y + 1 < entry_y {
for y in (branch_y + 1)..entry_y {
if entry_x < canvas.width && y < canvas.height {
set_route_edge_char(
canvas,
entry_x,
y,
coords.primary_edge_char(style),
style,
Some(fanout_owner),
);
}
}
}
for x in min_x..=max_x {
set_route_edge_char(canvas, x, branch_y, style.edge_h, style, Some(fanout_owner));
}
set_route_char(canvas, min_x, branch_y, style.corner_ul, Some(fanout_owner));
set_route_char(canvas, max_x, branch_y, style.corner_ur, Some(fanout_owner));
set_route_char(
canvas,
entry_x,
branch_y,
style.junction_down,
Some(fanout_owner),
);
for (tx, _, target) in target_positions {
let branch_owner_id = edge_route_owner_id(graph, &source.id, &target.id);
let branch_owner = RouteOwner {
kind: CellOwnerKind::EdgeSegment,
id: branch_owner_id.as_str(),
};
let (arrow_x, arrow_y) = adjusted_edge_entry_point(target, Direction::BT, graph);
if arrow_y + 1 < branch_y {
for y in (arrow_y + 1)..branch_y {
set_route_edge_char(canvas, tx, y, style.edge_v, style, Some(branch_owner));
}
}
set_route_char(
canvas,
arrow_x,
arrow_y,
coords.arrow_end(style),
Some(branch_owner),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{Direction, Graph, Node, Rectangle, Subgraph};
fn make_node(id: &str, x: usize, y: usize, width: usize, height: usize) -> Node {
let mut n = Node::new(id, id);
n.x = x;
n.y = y;
n.width = width;
n.height = height;
n
}
#[test]
fn exit_point_td_is_bottom_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_exit_point(&n, Direction::TD), (13, 8));
}
#[test]
fn exit_point_lr_is_right_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_exit_point(&n, Direction::LR), (16, 6));
}
#[test]
fn exit_point_rl_is_left_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_exit_point(&n, Direction::RL), (9, 6));
}
#[test]
fn exit_point_bt_is_top_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_exit_point(&n, Direction::BT), (13, 4));
}
#[test]
fn exit_point_rl_at_x0_saturates() {
let n = make_node("a", 0, 0, 6, 3);
assert_eq!(edge_exit_point(&n, Direction::RL), (0, 1));
}
#[test]
fn entry_point_td_is_above_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_entry_point(&n, Direction::TD), (13, 4));
}
#[test]
fn entry_point_lr_is_left_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_entry_point(&n, Direction::LR), (9, 6));
}
#[test]
fn entry_point_rl_is_right_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_entry_point(&n, Direction::RL), (16, 6));
}
#[test]
fn entry_point_bt_is_below_center() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(edge_entry_point(&n, Direction::BT), (13, 8));
}
#[test]
fn exit_and_entry_points_are_symmetric() {
let n = make_node("a", 10, 5, 6, 3);
assert_eq!(
edge_exit_point(&n, Direction::TD),
edge_entry_point(&n, Direction::BT)
);
assert_eq!(
edge_exit_point(&n, Direction::LR),
edge_entry_point(&n, Direction::RL)
);
assert_eq!(
edge_exit_point(&n, Direction::RL),
edge_entry_point(&n, Direction::LR)
);
assert_eq!(
edge_exit_point(&n, Direction::BT),
edge_entry_point(&n, Direction::TD)
);
}
fn graph_with_foreign_subgraph(sg_x: usize, sg_y: usize, sg_w: usize, sg_h: usize) -> Graph {
let mut g = Graph::new();
let mut sg = Subgraph::new("foreign", None);
sg.bounds = Rectangle::new(sg_x, sg_y, sg_w, sg_h);
g.add_subgraph(sg);
g
}
#[test]
fn hits_border_on_top_edge() {
let g = graph_with_foreign_subgraph(10, 5, 8, 6);
let n = make_node("n", 0, 0, 4, 3); assert!(hits_foreign_subgraph_border(&n, 14, 5, &g)); }
#[test]
fn hits_border_on_left_edge() {
let g = graph_with_foreign_subgraph(10, 5, 8, 6);
let n = make_node("n", 0, 0, 4, 3);
assert!(hits_foreign_subgraph_border(&n, 10, 8, &g)); }
#[test]
fn no_hit_interior_of_subgraph() {
let g = graph_with_foreign_subgraph(10, 5, 8, 6);
let n = make_node("n", 0, 0, 4, 3);
assert!(!hits_foreign_subgraph_border(&n, 13, 8, &g));
}
#[test]
fn no_hit_outside_subgraph() {
let g = graph_with_foreign_subgraph(10, 5, 8, 6);
let n = make_node("n", 0, 0, 4, 3);
assert!(!hits_foreign_subgraph_border(&n, 5, 5, &g)); assert!(!hits_foreign_subgraph_border(&n, 20, 8, &g)); }
#[test]
fn no_hit_for_own_subgraph() {
let mut g = Graph::new();
let mut sg = Subgraph::new("own", None);
sg.bounds = Rectangle::new(10, 5, 8, 6);
g.add_subgraph(sg);
g.add_node(make_node("n", 12, 6, 4, 3));
g.associate_node_with_subgraph("n", "own");
let n = g.get_node("n").expect("node 'n' was just added");
assert!(!hits_foreign_subgraph_border(n, 14, 5, &g));
}
#[test]
fn entry_candidates_td_starts_at_center() {
let n = make_node("a", 10, 5, 6, 3);
let candidates = edge_entry_candidates(&n, Direction::TD);
assert!(!candidates.is_empty());
let center_x = n.center_x(); assert_eq!(candidates[0], (center_x, n.y.saturating_sub(1)));
}
#[test]
fn entry_candidates_lr_starts_at_center() {
let n = make_node("a", 10, 5, 6, 3);
let candidates = edge_entry_candidates(&n, Direction::LR);
assert!(!candidates.is_empty());
let center_y = n.center_y(); assert_eq!(candidates[0], (n.x.saturating_sub(1), center_y));
}
#[test]
fn entry_candidates_no_duplicates() {
let n = make_node("a", 10, 5, 6, 3);
for dir in [Direction::TD, Direction::LR, Direction::RL, Direction::BT] {
let candidates = edge_entry_candidates(&n, dir);
let mut seen = std::collections::HashSet::new();
for pt in &candidates {
assert!(
seen.insert(*pt),
"duplicate candidate {pt:?} for direction {dir:?}"
);
}
}
}
}