use super::layout::{EdgePath, NodePosition, PathSegment};
use super::types::Orientation;
pub(crate) fn compute_routed_edges(
edge_pairs: &[(usize, usize)],
node_ids: &[String],
positions: &[NodePosition],
orientation: &Orientation,
) -> Vec<EdgePath> {
let mut paths = Vec::with_capacity(edge_pairs.len());
for &(from_idx, to_idx) in edge_pairs {
let Some(from_pos) = positions.get(from_idx) else {
continue;
};
let Some(to_pos) = positions.get(to_idx) else {
continue;
};
let from_id = node_ids.get(from_idx).cloned().unwrap_or_default();
let to_id = node_ids.get(to_idx).cloned().unwrap_or_default();
let segments = if from_idx == to_idx {
compute_self_loop(from_pos, orientation)
} else {
compute_orthogonal_route(from_pos, to_pos, orientation)
};
paths.push(EdgePath::new(from_id, to_id, segments));
}
nudge_parallel_edges(&mut paths);
paths
}
fn compute_orthogonal_route(
from: &NodePosition,
to: &NodePosition,
orientation: &Orientation,
) -> Vec<PathSegment> {
match orientation {
Orientation::LeftToRight => {
let start_x = from.x() + from.width();
let start_y = from.center_y();
let end_x = to.x();
let end_y = to.center_y();
if (start_y - end_y).abs() < 0.5 {
vec![
PathSegment::MoveTo(start_x, start_y),
PathSegment::LineTo(end_x, end_y),
]
} else {
let mid_x = (start_x + end_x) / 2.0;
vec![
PathSegment::MoveTo(start_x, start_y),
PathSegment::LineTo(mid_x, start_y),
PathSegment::LineTo(mid_x, end_y),
PathSegment::LineTo(end_x, end_y),
]
}
}
Orientation::TopToBottom => {
let start_x = from.center_x();
let start_y = from.y() + from.height();
let end_x = to.center_x();
let end_y = to.y();
if (start_x - end_x).abs() < 0.5 {
vec![
PathSegment::MoveTo(start_x, start_y),
PathSegment::LineTo(end_x, end_y),
]
} else {
let mid_y = (start_y + end_y) / 2.0;
vec![
PathSegment::MoveTo(start_x, start_y),
PathSegment::LineTo(start_x, mid_y),
PathSegment::LineTo(end_x, mid_y),
PathSegment::LineTo(end_x, end_y),
]
}
}
}
}
fn compute_self_loop(node: &NodePosition, orientation: &Orientation) -> Vec<PathSegment> {
let loop_offset = 2.0;
match orientation {
Orientation::LeftToRight => {
let exit_x = node.x() + node.width();
let exit_y = node.center_y() - 0.5;
let peak_x = exit_x + loop_offset;
let peak_y = node.y() - loop_offset;
vec![
PathSegment::MoveTo(exit_x, exit_y),
PathSegment::LineTo(peak_x, exit_y),
PathSegment::LineTo(peak_x, peak_y),
PathSegment::LineTo(exit_x - node.width() / 2.0, peak_y),
PathSegment::LineTo(exit_x - node.width() / 2.0, node.y()),
]
}
Orientation::TopToBottom => {
let exit_x = node.center_x() + 0.5;
let exit_y = node.y() + node.height();
let peak_y = exit_y + loop_offset;
let peak_x = node.x() + node.width() + loop_offset;
vec![
PathSegment::MoveTo(exit_x, exit_y),
PathSegment::LineTo(exit_x, peak_y),
PathSegment::LineTo(peak_x, peak_y),
PathSegment::LineTo(peak_x, node.center_y()),
PathSegment::LineTo(node.x() + node.width(), node.center_y()),
]
}
}
}
fn nudge_parallel_edges(paths: &mut [EdgePath]) {
let len = paths.len();
for i in 0..len {
for j in (i + 1)..len {
let same_pair = (paths[i].from_id() == paths[j].from_id()
&& paths[i].to_id() == paths[j].to_id())
|| (paths[i].from_id() == paths[j].to_id()
&& paths[i].to_id() == paths[j].from_id());
if same_pair {
offset_midpoints(&mut paths[i], 0.5);
offset_midpoints(&mut paths[j], -0.5);
}
}
}
}
fn offset_midpoints(path: &mut EdgePath, offset: f64) {
let segments = path.segments_mut();
let len = segments.len();
if len < 3 {
return;
}
for seg in segments[1..len - 1].iter_mut() {
match seg {
PathSegment::LineTo(_, y) | PathSegment::MoveTo(_, y) => {
*y += offset;
}
}
}
}
pub(crate) fn corner_char(
from_x: f64,
from_y: f64,
corner_x: f64,
corner_y: f64,
to_x: f64,
to_y: f64,
) -> &'static str {
let entering_horizontal = (from_y - corner_y).abs() < 0.5;
let leaving_horizontal = (corner_y - to_y).abs() < 0.5;
if entering_horizontal && !leaving_horizontal {
let from_left = from_x < corner_x;
let going_down = to_y > corner_y;
match (from_left, going_down) {
(true, true) => "\u{2510}", (true, false) => "\u{2518}", (false, true) => "\u{250c}", (false, false) => "\u{2514}", }
} else if !entering_horizontal && leaving_horizontal {
let from_above = from_y < corner_y;
let going_right = to_x > corner_x;
match (from_above, going_right) {
(true, true) => "\u{2514}", (true, false) => "\u{2518}", (false, true) => "\u{250c}", (false, false) => "\u{2510}", }
} else {
"\u{253c}" }
}
#[cfg(test)]
mod tests {
use super::*;
fn pos(id: &str, x: f64, y: f64) -> NodePosition {
NodePosition::new(id.to_string(), x, y, 12.0, 3.0)
}
#[test]
fn test_straight_horizontal_route() {
let positions = vec![pos("a", 0.0, 5.0), pos("b", 20.0, 5.0)];
let ids: Vec<String> = vec!["a".into(), "b".into()];
let paths = compute_routed_edges(&[(0, 1)], &ids, &positions, &Orientation::LeftToRight);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].from_id(), "a");
assert_eq!(paths[0].to_id(), "b");
assert_eq!(paths[0].segments().len(), 2);
}
#[test]
fn test_s_shaped_route_ltr() {
let positions = vec![pos("a", 0.0, 0.0), pos("b", 30.0, 10.0)];
let ids: Vec<String> = vec!["a".into(), "b".into()];
let paths = compute_routed_edges(&[(0, 1)], &ids, &positions, &Orientation::LeftToRight);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].segments().len(), 4);
}
#[test]
fn test_s_shaped_route_ttb() {
let positions = vec![pos("a", 5.0, 0.0), pos("b", 20.0, 15.0)];
let ids: Vec<String> = vec!["a".into(), "b".into()];
let paths = compute_routed_edges(&[(0, 1)], &ids, &positions, &Orientation::TopToBottom);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].segments().len(), 4);
}
#[test]
fn test_self_loop() {
let positions = vec![pos("a", 10.0, 10.0)];
let ids: Vec<String> = vec!["a".into()];
let paths = compute_routed_edges(&[(0, 0)], &ids, &positions, &Orientation::LeftToRight);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].from_id(), "a");
assert_eq!(paths[0].to_id(), "a");
assert!(paths[0].segments().len() >= 4); }
#[test]
fn test_parallel_edges_nudged() {
let positions = vec![pos("a", 0.0, 5.0), pos("b", 30.0, 5.0)];
let ids: Vec<String> = vec!["a".into(), "b".into()];
let paths = compute_routed_edges(
&[(0, 1), (1, 0)],
&ids,
&positions,
&Orientation::LeftToRight,
);
assert_eq!(paths.len(), 2);
assert_ne!(paths[0].segments(), paths[1].segments());
}
#[test]
fn test_corner_chars() {
assert_eq!(corner_char(0.0, 5.0, 10.0, 5.0, 10.0, 15.0), "\u{2510}");
assert_eq!(corner_char(0.0, 5.0, 10.0, 5.0, 10.0, 0.0), "\u{2518}");
assert_eq!(corner_char(20.0, 5.0, 10.0, 5.0, 10.0, 15.0), "\u{250c}");
assert_eq!(corner_char(20.0, 5.0, 10.0, 5.0, 10.0, 0.0), "\u{2514}");
}
#[test]
fn test_empty_edges() {
let paths = compute_routed_edges(&[], &[], &[], &Orientation::LeftToRight);
assert!(paths.is_empty());
}
#[test]
fn test_straight_vertical_route_ttb() {
let positions = vec![pos("a", 5.0, 0.0), pos("b", 5.0, 15.0)];
let ids: Vec<String> = vec!["a".into(), "b".into()];
let paths = compute_routed_edges(&[(0, 1)], &ids, &positions, &Orientation::TopToBottom);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].segments().len(), 2);
}
}