use super::super::attachments::plan_attachments;
use super::attachment_resolution::{
compute_attachment_plan as compute_attachment_plan_module,
resolve_attachment_points as resolve_attachment_points_module,
};
use super::draw_path::{
normalize_draw_path_points, repair_draw_path_segment_collisions, route_edge_from_draw_path,
};
use super::orthogonal::{
build_orthogonal_path as build_orthogonal_path_module,
compute_vertical_first_path as compute_vertical_first_path_module,
};
use super::path_selection::should_use_routed_draw_path;
use super::route_variants::route_backward_with_synthetic_waypoints as route_backward_with_synthetic_waypoints_module;
use super::self_edges::route_self_edge as route_self_edge_module;
use super::*;
use crate::graph::attachment::{
Face, OverflowSide, canonical_backward_channel_face, classify_face_float, edge_faces,
fan_in_overflow_face_for_slot, fan_in_primary_face_capacity, point_on_face_float,
resolve_overflow_backward_channel_conflict,
};
use crate::graph::grid::{
GridLayout, GridPos, NodeBounds, NodeFace, SelfEdgeDrawData, SubgraphBounds,
};
use crate::graph::routing::build_orthogonal_path_float;
use crate::graph::space::{FPoint, FRect};
use crate::graph::{Direction, Edge, Graph, Node};
fn simple_td_diagram() -> (Graph, GridLayout) {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("End"));
diagram.add_edge(Edge::new("A", "B"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(10, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
],
&[],
);
(diagram, layout)
}
#[test]
fn test_route_edge_straight_vertical() {
let (diagram, layout) = simple_td_diagram();
let edge = &diagram.edges[0];
let routed = route_edge(edge, &layout, Direction::TopDown, None, None, false).unwrap();
assert!(!routed.segments.is_empty());
if routed.start.x == routed.end.x {
assert!(
!routed.segments.is_empty(),
"Expected at least 1 segment, got {}",
routed.segments.len()
);
for seg in &routed.segments {
match seg {
Segment::Vertical { x, .. } => {
assert_eq!(
*x, routed.start.x as usize,
"Vertical segment should be colinear with start/end"
);
}
_ => panic!(
"Expected all vertical segments for colinear nodes, got {:?}",
seg
),
}
}
}
}
#[test]
fn test_route_edge_with_bend() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("Branch1"));
diagram.add_node(Node::new("C").with_label("Branch2"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("A", "C"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(20, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
("C", make_bounds_sized(30, 13, 7, 3)),
],
&[],
);
let edge = &diagram.edges[1];
let routed = route_edge(edge, &layout, Direction::TopDown, None, None, false).unwrap();
if routed.start.x != routed.end.x {
assert!(routed.segments.len() > 1);
}
}
#[test]
fn test_route_all_edges() {
let (diagram, layout) = simple_td_diagram();
let routed = route_all_edges(&diagram.edges, &layout, Direction::TopDown);
assert_eq!(routed.len(), 1);
}
#[test]
fn test_attachment_directions_td() {
let (out_dir, in_dir) = attachment_directions(Direction::TopDown);
assert!(matches!(out_dir, AttachDirection::Bottom));
assert!(matches!(in_dir, AttachDirection::Top));
}
#[test]
fn test_attachment_directions_lr() {
let (out_dir, in_dir) = attachment_directions(Direction::LeftRight);
assert!(matches!(out_dir, AttachDirection::Right));
assert!(matches!(in_dir, AttachDirection::Left));
}
#[test]
fn test_point_creation() {
let p = Point::new(10, 20);
assert_eq!(p.x, 10);
assert_eq!(p.y, 20);
}
#[test]
fn test_straight_vertical_path() {
let start = Point::new(10, 5);
let end = Point::new(10, 15);
let segments = compute_vertical_first_path(start, end);
assert_eq!(segments.len(), 1);
match segments[0] {
Segment::Vertical { x, y_start, y_end } => {
assert_eq!(x, 10);
assert_eq!(y_start, 5);
assert_eq!(y_end, 15);
}
_ => panic!("Expected vertical segment"),
}
}
#[test]
fn test_z_shaped_vertical_path() {
let start = Point::new(5, 5);
let end = Point::new(15, 15);
let segments = compute_vertical_first_path(start, end);
assert_eq!(segments.len(), 3);
assert!(matches!(segments[0], Segment::Vertical { .. }));
assert!(matches!(segments[1], Segment::Horizontal { .. }));
assert!(matches!(segments[2], Segment::Vertical { .. }));
}
fn make_bounds(x: usize, y: usize) -> NodeBounds {
make_bounds_sized(x, y, 10, 3)
}
fn make_bounds_sized(x: usize, y: usize, width: usize, height: usize) -> NodeBounds {
NodeBounds {
x,
y,
width,
height,
layout_center_x: None,
layout_center_y: None,
}
}
fn minimal_layout(
bounds: &[(&str, NodeBounds)],
routed_paths: &[(usize, Vec<(usize, usize)>)],
) -> GridLayout {
let node_bounds: std::collections::HashMap<String, NodeBounds> = bounds
.iter()
.map(|(id, bounds)| ((*id).to_string(), *bounds))
.collect();
let draw_positions: std::collections::HashMap<String, (usize, usize)> = bounds
.iter()
.map(|(id, bounds)| ((*id).to_string(), (bounds.x, bounds.y)))
.collect();
let grid_positions: std::collections::HashMap<String, GridPos> = bounds
.iter()
.enumerate()
.map(|(idx, (id, _))| {
(
(*id).to_string(),
GridPos {
layer: idx,
pos: idx,
},
)
})
.collect();
let node_shapes: std::collections::HashMap<String, Shape> = bounds
.iter()
.map(|(id, _)| ((*id).to_string(), Shape::Rectangle))
.collect();
let routed_edge_paths: std::collections::HashMap<usize, Vec<(usize, usize)>> = routed_paths
.iter()
.map(|(edge_idx, points)| (*edge_idx, points.clone()))
.collect();
GridLayout {
grid_positions,
draw_positions,
node_bounds,
width: 120,
height: 80,
h_spacing: 4,
v_spacing: 3,
edge_waypoints: std::collections::HashMap::new(),
routed_edge_paths,
preserve_routed_path_topology: std::collections::HashSet::new(),
edge_label_positions: std::collections::HashMap::new(),
node_shapes,
subgraph_bounds: std::collections::HashMap::new(),
self_edges: Vec::new(),
node_directions: std::collections::HashMap::new(),
}
}
fn default_routing_overrides() -> RoutingOverrides {
RoutingOverrides {
src_attach: None,
tgt_attach: None,
src_face: None,
tgt_face: None,
src_first_vertical: false,
}
}
#[test]
fn test_is_backward_edge_td_forward() {
let from = make_bounds(10, 0);
let to = make_bounds(10, 10);
assert!(!is_backward_edge(&from, &to, Direction::TopDown));
}
#[test]
fn test_is_backward_edge_td_backward() {
let from = make_bounds(10, 10);
let to = make_bounds(10, 0);
assert!(is_backward_edge(&from, &to, Direction::TopDown));
}
#[test]
fn test_is_backward_edge_bt_forward() {
let from = make_bounds(10, 10);
let to = make_bounds(10, 0);
assert!(!is_backward_edge(&from, &to, Direction::BottomTop));
}
#[test]
fn test_is_backward_edge_bt_backward() {
let from = make_bounds(10, 0);
let to = make_bounds(10, 10);
assert!(is_backward_edge(&from, &to, Direction::BottomTop));
}
#[test]
fn test_is_backward_edge_lr_forward() {
let from = make_bounds(0, 10);
let to = make_bounds(20, 10);
assert!(!is_backward_edge(&from, &to, Direction::LeftRight));
}
#[test]
fn test_is_backward_edge_lr_backward() {
let from = make_bounds(20, 10);
let to = make_bounds(0, 10);
assert!(is_backward_edge(&from, &to, Direction::LeftRight));
}
#[test]
fn test_is_backward_edge_rl_forward() {
let from = make_bounds(20, 10);
let to = make_bounds(0, 10);
assert!(!is_backward_edge(&from, &to, Direction::RightLeft));
}
#[test]
fn test_is_backward_edge_rl_backward() {
let from = make_bounds(0, 10);
let to = make_bounds(20, 10);
assert!(is_backward_edge(&from, &to, Direction::RightLeft));
}
#[test]
fn test_is_backward_edge_same_position() {
let from = make_bounds(10, 10);
let to = make_bounds(10, 10);
assert!(!is_backward_edge(&from, &to, Direction::TopDown));
assert!(!is_backward_edge(&from, &to, Direction::BottomTop));
assert!(!is_backward_edge(&from, &to, Direction::LeftRight));
assert!(!is_backward_edge(&from, &to, Direction::RightLeft));
}
#[test]
fn test_orthogonalize_segment_vertical() {
let from = Point::new(10, 5);
let to = Point::new(10, 15);
let segments = orthogonalize_segment(from, to, true);
assert_eq!(segments.len(), 1);
match segments[0] {
Segment::Vertical { x, y_start, y_end } => {
assert_eq!(x, 10);
assert_eq!(y_start, 5);
assert_eq!(y_end, 15);
}
_ => panic!("Expected vertical segment"),
}
}
#[test]
fn test_orthogonalize_segment_horizontal() {
let from = Point::new(5, 10);
let to = Point::new(20, 10);
let segments = orthogonalize_segment(from, to, true);
assert_eq!(segments.len(), 1);
match segments[0] {
Segment::Horizontal { y, x_start, x_end } => {
assert_eq!(y, 10);
assert_eq!(x_start, 5);
assert_eq!(x_end, 20);
}
_ => panic!("Expected horizontal segment"),
}
}
#[test]
fn test_orthogonalize_segment_diagonal_vertical_first() {
let from = Point::new(5, 5);
let to = Point::new(15, 20);
let segments = orthogonalize_segment(from, to, true);
assert_eq!(segments.len(), 2);
match segments[0] {
Segment::Vertical { x, y_start, y_end } => {
assert_eq!(x, 5);
assert_eq!(y_start, 5);
assert_eq!(y_end, 20);
}
_ => panic!("Expected vertical segment first"),
}
match segments[1] {
Segment::Horizontal { y, x_start, x_end } => {
assert_eq!(y, 20);
assert_eq!(x_start, 5);
assert_eq!(x_end, 15);
}
_ => panic!("Expected horizontal segment second"),
}
}
#[test]
fn test_orthogonalize_segment_diagonal_horizontal_first() {
let from = Point::new(5, 5);
let to = Point::new(15, 20);
let segments = orthogonalize_segment(from, to, false);
assert_eq!(segments.len(), 2);
match segments[0] {
Segment::Horizontal { y, x_start, x_end } => {
assert_eq!(y, 5);
assert_eq!(x_start, 5);
assert_eq!(x_end, 15);
}
_ => panic!("Expected horizontal segment first"),
}
match segments[1] {
Segment::Vertical { x, y_start, y_end } => {
assert_eq!(x, 15);
assert_eq!(y_start, 5);
assert_eq!(y_end, 20);
}
_ => panic!("Expected vertical segment second"),
}
}
#[test]
fn test_orthogonalize_empty_waypoints() {
let waypoints: Vec<(usize, usize)> = vec![];
let segments = orthogonalize(&waypoints, Direction::TopDown);
assert!(segments.is_empty());
}
#[test]
fn test_orthogonalize_single_waypoint() {
let waypoints = vec![(10, 10)];
let segments = orthogonalize(&waypoints, Direction::TopDown);
assert!(segments.is_empty());
}
#[test]
fn test_orthogonalize_two_waypoints_aligned() {
let waypoints = vec![(10, 5), (10, 15)];
let segments = orthogonalize(&waypoints, Direction::TopDown);
assert_eq!(segments.len(), 1);
assert!(matches!(segments[0], Segment::Vertical { x: 10, .. }));
}
#[test]
fn test_orthogonalize_two_waypoints_diagonal() {
let waypoints = vec![(5, 5), (15, 20)];
let segments = orthogonalize(&waypoints, Direction::TopDown);
assert_eq!(segments.len(), 2);
assert!(matches!(segments[0], Segment::Vertical { .. }));
assert!(matches!(segments[1], Segment::Horizontal { .. }));
}
#[test]
fn test_orthogonalize_three_waypoints() {
let waypoints = vec![(5, 5), (15, 10), (25, 20)];
let segments = orthogonalize(&waypoints, Direction::TopDown);
assert_eq!(segments.len(), 4);
}
#[test]
fn test_build_orthogonal_path_no_waypoints() {
let start = Point::new(10, 5);
let end = Point::new(20, 15);
let waypoints: Vec<(usize, usize)> = vec![];
let segments = build_orthogonal_path(start, &waypoints, end, Direction::TopDown);
assert_eq!(segments.len(), 2);
assert!(matches!(segments[0], Segment::Vertical { .. }));
assert!(matches!(segments[1], Segment::Horizontal { .. }));
}
#[test]
fn test_build_orthogonal_path_with_waypoints() {
let start = Point::new(10, 5);
let waypoints = vec![(15, 10), (20, 15)];
let end = Point::new(25, 20);
let segments = build_orthogonal_path(start, &waypoints, end, Direction::TopDown);
assert_eq!(segments.len(), 6);
}
#[test]
fn test_build_orthogonal_path_aligned_waypoints() {
let start = Point::new(10, 5);
let waypoints = vec![(10, 10), (10, 15)]; let end = Point::new(10, 20);
let segments = build_orthogonal_path(start, &waypoints, end, Direction::TopDown);
assert_eq!(segments.len(), 3);
for seg in segments {
assert!(matches!(seg, Segment::Vertical { x: 10, .. }));
}
}
#[test]
fn test_build_orthogonal_path_lr_direction() {
let start = Point::new(5, 10);
let end = Point::new(20, 15);
let waypoints: Vec<(usize, usize)> = vec![];
let segments = build_orthogonal_path(start, &waypoints, end, Direction::LeftRight);
assert_eq!(segments.len(), 2);
assert!(matches!(segments[0], Segment::Horizontal { .. }));
assert!(matches!(segments[1], Segment::Vertical { .. }));
}
#[test]
fn attachment_plan_stays_stable_for_shared_face_edges() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("Left"));
diagram.add_node(Node::new("C").with_label("Right"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("A", "C"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(20, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
("C", make_bounds_sized(30, 13, 7, 3)),
],
&[],
);
let plan = compute_attachment_plan_module(&diagram.edges, &layout, Direction::TopDown);
assert!(
!plan.is_empty(),
"shared-face routing should keep attachment overrides stable"
);
}
#[test]
fn attachment_resolution_module_keeps_lr_consensus_y() {
let from = make_bounds_sized(0, 2, 10, 3);
let to = make_bounds_sized(20, 4, 10, 5);
let ep = EdgeEndpoints {
from_bounds: from,
from_shape: Shape::Rectangle,
to_bounds: to,
to_shape: Shape::Rectangle,
};
let (src, tgt) = resolve_attachment_points_module(None, None, &ep, &[], Direction::LeftRight);
assert_eq!(src.1, tgt.1);
}
#[test]
fn orthogonal_builder_module_preserves_vertical_first_z_path() {
let start = Point::new(5, 5);
let end = Point::new(15, 15);
let vertical_first = compute_vertical_first_path_module(start, end);
let through_waypoints =
build_orthogonal_path_module(start, &[(10, 10)], end, Direction::TopDown);
assert!(matches!(vertical_first[0], Segment::Vertical { .. }));
assert!(matches!(through_waypoints[0], Segment::Vertical { .. }));
}
#[test]
fn test_route_backward_edge_td() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("End"));
diagram.add_edge(Edge::new("A", "B")); diagram.add_edge(Edge::new("B", "A"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(10, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
],
&[],
);
let backward_edge = &diagram.edges[1];
let routed = route_edge(
backward_edge,
&layout,
Direction::TopDown,
None,
None,
false,
)
.unwrap();
assert!(routed.is_backward);
assert!(!routed.segments.is_empty());
}
#[test]
fn route_edge_reports_shared_routed_draw_path_when_backward_draw_path_is_used() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Bottom"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "A"));
let source = make_bounds_sized(20, 20, 10, 3);
let target = make_bounds_sized(20, 0, 10, 3);
let draw_path = vec![(30, 21), (40, 21), (40, 1), (30, 1)];
let layout = minimal_layout(
&[("A", target), ("B", source)],
&[(diagram.edges[1].index, draw_path)],
);
let result = route_edge_with_probe(
&diagram.edges[1],
&layout,
Direction::TopDown,
None,
None,
false,
)
.expect("backward routed draw path should route");
assert_eq!(
result.probe.path_family,
TextPathFamily::SharedRoutedDrawPath
);
assert_eq!(result.probe.rejection_reason, None);
}
#[test]
fn lr_backward_waypoints_prefer_waypoint_inferred_bottom_faces() {
let from = make_bounds_sized(20, 2, 10, 3);
let to = make_bounds_sized(4, 2, 10, 3);
let endpoints = EdgeEndpoints {
from_bounds: from,
from_shape: Shape::Rectangle,
to_bounds: to,
to_shape: Shape::Rectangle,
};
let waypoints = vec![(20, 7), (14, 7)];
let (src_attach, tgt_attach) =
resolve_attachment_points_module(None, None, &endpoints, &waypoints, Direction::LeftRight);
assert_eq!(src_attach.1, from.y + from.height - 1);
assert_eq!(tgt_attach.1, to.y + to.height - 1);
}
#[test]
fn route_edge_reports_rejection_reason_when_draw_path_hits_unrelated_node() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Bottom"));
diagram.add_node(Node::new("C").with_label("Blocker"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "A"));
let source = make_bounds_sized(20, 20, 10, 3);
let target = make_bounds_sized(20, 0, 10, 3);
let blocker = make_bounds_sized(38, 10, 8, 4);
let draw_path = vec![(30, 21), (40, 21), (40, 1), (30, 1)];
let layout = minimal_layout(
&[("A", target), ("B", source), ("C", blocker)],
&[(diagram.edges[1].index, draw_path)],
);
let result = route_edge_with_probe(
&diagram.edges[1],
&layout,
Direction::TopDown,
None,
None,
false,
)
.expect("backward edge should fall back after draw-path rejection");
assert_eq!(result.probe.path_family, TextPathFamily::SyntheticBackward);
assert_eq!(
result.probe.rejection_reason,
Some(TextPathRejection::SegmentCollision)
);
}
#[test]
fn route_variants_module_routes_backward_waypoints() {
let edge = Edge::new("B", "A");
let endpoints = EdgeEndpoints {
from_bounds: make_bounds_sized(20, 20, 10, 3),
from_shape: Shape::Rectangle,
to_bounds: make_bounds_sized(20, 0, 10, 3),
to_shape: Shape::Rectangle,
};
let routed = route_backward_with_synthetic_waypoints_module(
&edge,
&endpoints,
&[(35, 21), (35, 1)],
Direction::TopDown,
default_routing_overrides(),
)
.expect("synthetic backward waypoint route should succeed");
assert!(routed.is_backward);
assert!(!routed.segments.is_empty());
}
#[test]
fn self_edge_module_preserves_top_down_loop_entry_direction() {
let edge = Edge::new("A", "A");
let self_edge = SelfEdgeDrawData {
node_id: "A".to_string(),
edge_index: 0,
points: vec![(10, 5), (14, 5), (14, 9), (10, 9), (10, 5)],
};
let routed = route_self_edge_module(&self_edge, &edge, Direction::TopDown);
assert!(routed.is_self_edge);
assert_eq!(routed.entry_direction, AttachDirection::Right);
}
#[test]
fn path_selection_prefers_structured_forward_routed_draw_path() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Bottom"));
diagram.add_edge(Edge::new("A", "B"));
let source = make_bounds_sized(20, 0, 10, 3);
let target = make_bounds_sized(20, 20, 10, 3);
let draw_path = vec![(25, 2), (25, 6), (35, 6), (35, 16), (25, 16), (25, 20)];
let mut layout = minimal_layout(&[("A", source), ("B", target)], &[(0, draw_path)]);
layout
.edge_waypoints
.insert(diagram.edges[0].index, vec![(25, 6), (35, 16)]);
assert!(should_use_routed_draw_path(
&diagram.edges[0],
&layout,
&source,
&target,
diagram.direction,
None,
));
}
#[test]
fn draw_path_route_reports_segment_collision_from_helper_module() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Bottom"));
diagram.add_node(Node::new("C").with_label("Blocker"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "A"));
let source = make_bounds_sized(20, 20, 10, 3);
let target = make_bounds_sized(20, 0, 10, 3);
let blocker = make_bounds_sized(38, 10, 8, 4);
let draw_path = vec![(30, 21), (40, 21), (40, 1), (30, 1)];
let layout = minimal_layout(
&[("A", target), ("B", source), ("C", blocker)],
&[(diagram.edges[1].index, draw_path.clone())],
);
let endpoints = EdgeEndpoints {
from_bounds: source,
from_shape: Shape::Rectangle,
to_bounds: target,
to_shape: Shape::Rectangle,
};
let rejection = route_edge_from_draw_path(
&diagram.edges[1],
&layout,
&endpoints,
&draw_path,
Direction::TopDown,
default_routing_overrides(),
)
.expect_err("blocking draw path should reject instead of routing through blocker");
assert_eq!(rejection, TextPathRejection::SegmentCollision);
}
#[test]
fn draw_path_collision_repair_detours_around_blocker() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Left"));
diagram.add_node(Node::new("B").with_label("Right"));
diagram.add_node(Node::new("C").with_label("Blocker"));
diagram.add_edge(Edge::new("A", "B"));
let source = make_bounds_sized(0, 4, 8, 3);
let target = make_bounds_sized(20, 4, 8, 3);
let blocker = make_bounds_sized(10, 4, 4, 3);
let layout = minimal_layout(&[("A", source), ("B", target), ("C", blocker)], &[]);
let repaired =
repair_draw_path_segment_collisions(&[(7, 5), (21, 5)], &layout, &diagram.edges[0]);
assert_eq!(repaired, vec![(7, 5), (7, 3), (21, 3), (21, 5)]);
}
#[test]
fn draw_path_normalization_repairs_terminal_staircase() {
let normalized = normalize_draw_path_points(
&[(12, 0), (12, 2), (12, 5), (18, 5), (18, 8)],
Direction::TopDown,
);
assert_eq!(
normalized,
vec![(12, 0), (12, 2), (12, 4), (18, 4), (18, 8)]
);
}
#[test]
fn test_forward_edge_entry_direction_td() {
let (diagram, layout) = simple_td_diagram();
let edge = &diagram.edges[0];
let routed = route_edge(edge, &layout, Direction::TopDown, None, None, false).unwrap();
assert_eq!(routed.entry_direction, AttachDirection::Top);
}
#[test]
fn test_forward_edge_entry_direction_lr() {
let mut diagram = Graph::new(Direction::LeftRight);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("End"));
diagram.add_edge(Edge::new("A", "B"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(3, 10, 7, 3)),
("B", make_bounds_sized(20, 10, 7, 3)),
],
&[],
);
let edge = &diagram.edges[0];
let routed = route_edge(edge, &layout, Direction::LeftRight, None, None, false).unwrap();
assert_eq!(routed.entry_direction, AttachDirection::Left);
}
#[test]
fn test_multiple_backward_edges_route_successfully() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Middle"));
diagram.add_node(Node::new("C").with_label("Bottom"));
diagram.add_edge(Edge::new("A", "B")); diagram.add_edge(Edge::new("B", "C")); diagram.add_edge(Edge::new("C", "A")); diagram.add_edge(Edge::new("C", "B"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(10, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
("C", make_bounds_sized(10, 23, 7, 3)),
],
&[],
);
let edge_c_to_a = &diagram.edges[2];
let edge_c_to_b = &diagram.edges[3];
let routed_c_a = route_edge(edge_c_to_a, &layout, Direction::TopDown, None, None, false);
let routed_c_b = route_edge(edge_c_to_b, &layout, Direction::TopDown, None, None, false);
assert!(routed_c_a.is_some(), "Backward edge C->A should route");
assert!(routed_c_b.is_some(), "Backward edge C->B should route");
assert!(!routed_c_a.unwrap().segments.is_empty());
assert!(!routed_c_b.unwrap().segments.is_empty());
}
#[test]
fn test_backward_edge_with_waypoints_td() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Middle"));
diagram.add_node(Node::new("C").with_label("Bottom"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "C"));
diagram.add_edge(Edge::new("C", "A"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(10, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
("C", make_bounds_sized(10, 23, 7, 3)),
],
&[],
);
let backward_edge = &diagram.edges[2];
let routed = route_edge(
backward_edge,
&layout,
Direction::TopDown,
None,
None,
false,
)
.unwrap();
assert!(
routed.segments.len() >= 2,
"Backward edge should have routing segments, got {}",
routed.segments.len()
);
}
#[test]
fn test_short_backward_edge_uses_synthetic_waypoints() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A").with_label("Top"));
diagram.add_node(Node::new("B").with_label("Bottom"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "A"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(10, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
],
&[],
);
let backward_edge = &diagram.edges[1];
let routed = route_edge(
backward_edge,
&layout,
Direction::TopDown,
None,
None,
false,
);
assert!(routed.is_some(), "Backward edge should route successfully");
let routed = routed.unwrap();
assert!(
routed.segments.len() >= 3,
"Backward edge with synthetic waypoints should have >= 3 segments, got {}",
routed.segments.len()
);
}
#[test]
fn test_backward_edge_lr_with_waypoints() {
let mut diagram = Graph::new(Direction::LeftRight);
diagram.add_node(Node::new("A").with_label("Left"));
diagram.add_node(Node::new("B").with_label("Mid"));
diagram.add_node(Node::new("C").with_label("Right"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "C"));
diagram.add_edge(Edge::new("C", "A"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(3, 10, 7, 3)),
("B", make_bounds_sized(20, 10, 7, 3)),
("C", make_bounds_sized(37, 10, 7, 3)),
],
&[],
);
let backward_edge = &diagram.edges[2];
let routed = route_edge(
backward_edge,
&layout,
Direction::LeftRight,
None,
None,
false,
);
assert!(
routed.is_some(),
"LR backward edge should route successfully"
);
}
#[test]
fn test_lr_zero_gap_entry_direction() {
let mut diagram = Graph::new(Direction::LeftRight);
diagram.add_node(Node::new("Input").with_label("User Input"));
diagram.add_node(Node::new("Process").with_label("Process Data"));
diagram.add_node(Node::new("Output").with_label("Display Result"));
diagram.add_edge(Edge::new("Input", "Process"));
diagram.add_edge(Edge::new("Process", "Output"));
let layout = minimal_layout(
&[
("Input", make_bounds_sized(3, 10, 12, 3)),
("Process", make_bounds_sized(20, 10, 14, 3)),
("Output", make_bounds_sized(39, 10, 16, 3)),
],
&[],
);
let routed_edges = route_all_edges(&diagram.edges, &layout, Direction::LeftRight);
for routed in &routed_edges {
assert_eq!(
routed.entry_direction,
AttachDirection::Left,
"LR edge {}->{} should enter from Left, not {:?}",
routed.edge.from,
routed.edge.to,
routed.entry_direction,
);
}
}
#[test]
fn test_lr_attachment_consensus_y_same_height() {
let from = make_bounds_sized(0, 2, 10, 3);
let to = make_bounds_sized(20, 4, 10, 3);
let ep = EdgeEndpoints {
from_bounds: from,
from_shape: Shape::Rectangle,
to_bounds: to,
to_shape: Shape::Rectangle,
};
let (src, tgt) = resolve_attachment_points_module(None, None, &ep, &[], Direction::LeftRight);
assert_eq!(
src.1, tgt.1,
"LR attachment points should have consensus y, got src.y={} tgt.y={}",
src.1, tgt.1
);
}
#[test]
fn test_lr_attachment_consensus_y_different_height() {
let from = make_bounds_sized(0, 2, 10, 3);
let to = make_bounds_sized(20, 3, 10, 5);
let ep = EdgeEndpoints {
from_bounds: from,
from_shape: Shape::Rectangle,
to_bounds: to,
to_shape: Shape::Rectangle,
};
let (src, tgt) = resolve_attachment_points_module(None, None, &ep, &[], Direction::LeftRight);
assert_eq!(
src.1, tgt.1,
"LR attachment points should have consensus y even with different heights, got src.y={} tgt.y={}",
src.1, tgt.1
);
}
#[test]
fn test_rl_attachment_consensus_y() {
let from = make_bounds_sized(20, 2, 10, 3);
let to = make_bounds_sized(0, 4, 10, 3);
let ep = EdgeEndpoints {
from_bounds: from,
from_shape: Shape::Rectangle,
to_bounds: to,
to_shape: Shape::Rectangle,
};
let (src, tgt) = resolve_attachment_points_module(None, None, &ep, &[], Direction::RightLeft);
assert_eq!(
src.1, tgt.1,
"RL attachment points should have consensus y, got src.y={} tgt.y={}",
src.1, tgt.1
);
}
#[test]
fn test_generate_backward_waypoints_td() {
let src = make_bounds_sized(4, 6, 8, 3);
let tgt = make_bounds_sized(4, 0, 8, 3);
let waypoints = generate_backward_waypoints(&src, &tgt, Direction::TopDown);
assert!(!waypoints.is_empty(), "should produce waypoints");
let max_right = (src.x + src.width).max(tgt.x + tgt.width);
for wp in &waypoints {
assert!(
wp.0 > max_right,
"waypoint x={} should be right of nodes (max_right={})",
wp.0,
max_right
);
}
}
#[test]
fn test_generate_backward_waypoints_lr() {
let src = make_bounds_sized(12, 2, 8, 3);
let tgt = make_bounds_sized(0, 2, 8, 3);
let waypoints = generate_backward_waypoints(&src, &tgt, Direction::LeftRight);
assert!(!waypoints.is_empty(), "should produce waypoints");
let max_bottom = (src.y + src.height).max(tgt.y + tgt.height);
for wp in &waypoints {
assert!(
wp.1 > max_bottom,
"waypoint y={} should be below nodes (max_bottom={})",
wp.1,
max_bottom
);
}
}
#[test]
fn test_generate_backward_waypoints_forward_returns_empty() {
let src = make_bounds_sized(4, 0, 8, 3);
let tgt = make_bounds_sized(4, 6, 8, 3);
let waypoints = generate_backward_waypoints(&src, &tgt, Direction::TopDown);
assert!(
waypoints.is_empty(),
"forward edge should return empty waypoints"
);
}
#[test]
fn test_generate_backward_waypoints_bt() {
let src = make_bounds_sized(4, 0, 8, 3);
let tgt = make_bounds_sized(4, 6, 8, 3);
let waypoints = generate_backward_waypoints(&src, &tgt, Direction::BottomTop);
assert!(
!waypoints.is_empty(),
"should produce waypoints for BT backward"
);
let max_right = (src.x + src.width).max(tgt.x + tgt.width);
for wp in &waypoints {
assert!(wp.0 > max_right, "BT waypoint should be right of nodes");
}
}
#[test]
fn test_generate_backward_waypoints_rl() {
let src = make_bounds_sized(0, 2, 8, 3);
let tgt = make_bounds_sized(12, 2, 8, 3);
let waypoints = generate_backward_waypoints(&src, &tgt, Direction::RightLeft);
assert!(
!waypoints.is_empty(),
"should produce waypoints for RL backward"
);
let max_bottom = (src.y + src.height).max(tgt.y + tgt.height);
for wp in &waypoints {
assert!(wp.1 > max_bottom, "RL waypoint should be below nodes");
}
}
#[test]
fn vertical_segment_length() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.length(), 10);
}
#[test]
fn vertical_segment_length_reversed() {
let seg = Segment::Vertical {
x: 5,
y_start: 20,
y_end: 10,
};
assert_eq!(seg.length(), 10);
}
#[test]
fn horizontal_segment_length() {
let seg = Segment::Horizontal {
y: 3,
x_start: 5,
x_end: 15,
};
assert_eq!(seg.length(), 10);
}
#[test]
fn horizontal_segment_length_reversed() {
let seg = Segment::Horizontal {
y: 3,
x_start: 15,
x_end: 5,
};
assert_eq!(seg.length(), 10);
}
#[test]
fn zero_length_segment() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 10,
};
assert_eq!(seg.length(), 0);
}
#[test]
fn start_point_vertical() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.start_point(), Point { x: 5, y: 10 });
}
#[test]
fn end_point_vertical() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.end_point(), Point { x: 5, y: 20 });
}
#[test]
fn start_point_horizontal() {
let seg = Segment::Horizontal {
y: 3,
x_start: 5,
x_end: 15,
};
assert_eq!(seg.start_point(), Point { x: 5, y: 3 });
}
#[test]
fn end_point_horizontal() {
let seg = Segment::Horizontal {
y: 3,
x_start: 5,
x_end: 15,
};
assert_eq!(seg.end_point(), Point { x: 15, y: 3 });
}
#[test]
fn point_at_offset_zero_is_start() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.point_at_offset(0), seg.start_point());
}
#[test]
fn point_at_offset_length_is_end() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.point_at_offset(seg.length()), seg.end_point());
}
#[test]
fn point_at_offset_midpoint_vertical() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.point_at_offset(5), Point { x: 5, y: 15 });
}
#[test]
fn point_at_offset_midpoint_horizontal() {
let seg = Segment::Horizontal {
y: 3,
x_start: 0,
x_end: 10,
};
assert_eq!(seg.point_at_offset(5), Point { x: 5, y: 3 });
}
#[test]
fn point_at_offset_reversed_vertical() {
let seg = Segment::Vertical {
x: 5,
y_start: 20,
y_end: 10,
};
assert_eq!(seg.point_at_offset(5), Point { x: 5, y: 15 });
}
#[test]
fn point_at_offset_reversed_horizontal() {
let seg = Segment::Horizontal {
y: 3,
x_start: 15,
x_end: 5,
};
assert_eq!(seg.point_at_offset(5), Point { x: 10, y: 3 });
}
#[test]
fn point_at_offset_clamped_beyond_length() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 20,
};
assert_eq!(seg.point_at_offset(100), seg.end_point());
}
#[test]
fn point_at_offset_zero_length_segment() {
let seg = Segment::Vertical {
x: 5,
y_start: 10,
y_end: 10,
};
assert_eq!(seg.point_at_offset(0), Point { x: 5, y: 10 });
}
#[test]
fn routing_core_edge_faces_all_directions_forward_and_backward() {
assert_eq!(
edge_faces(Direction::TopDown, false),
(Face::Bottom, Face::Top)
);
assert_eq!(
edge_faces(Direction::TopDown, true),
(Face::Top, Face::Bottom)
);
assert_eq!(
edge_faces(Direction::BottomTop, false),
(Face::Top, Face::Bottom)
);
assert_eq!(
edge_faces(Direction::BottomTop, true),
(Face::Bottom, Face::Top)
);
assert_eq!(
edge_faces(Direction::LeftRight, false),
(Face::Right, Face::Left)
);
assert_eq!(
edge_faces(Direction::LeftRight, true),
(Face::Left, Face::Right)
);
assert_eq!(
edge_faces(Direction::RightLeft, false),
(Face::Left, Face::Right)
);
assert_eq!(
edge_faces(Direction::RightLeft, true),
(Face::Right, Face::Left)
);
}
#[test]
fn routing_core_classify_face_float_prefers_major_axis() {
let center = FPoint::new(50.0, 50.0);
let rect = FRect::new(40.0, 40.0, 20.0, 20.0);
assert_eq!(
classify_face_float(center, rect, FPoint::new(50.0, 10.0)),
Face::Top
);
assert_eq!(
classify_face_float(center, rect, FPoint::new(90.0, 50.0)),
Face::Right
);
}
#[test]
fn routing_core_point_on_face_float_uses_fraction_and_clamps() {
let rect = FRect::new(10.0, 20.0, 40.0, 20.0);
assert_eq!(
point_on_face_float(rect, Face::Top, 0.0),
FPoint::new(10.0, 20.0)
);
assert_eq!(
point_on_face_float(rect, Face::Top, 1.0),
FPoint::new(50.0, 20.0)
);
assert_eq!(
point_on_face_float(rect, Face::Right, -2.0),
FPoint::new(50.0, 20.0)
);
assert_eq!(
point_on_face_float(rect, Face::Left, 2.0),
FPoint::new(10.0, 40.0)
);
}
#[test]
fn routing_core_face_conversions_are_explicit_and_lossless() {
assert_eq!(Face::from_node_face(NodeFace::Top), Face::Top);
assert_eq!(Face::from_node_face(NodeFace::Bottom), Face::Bottom);
assert_eq!(Face::from_node_face(NodeFace::Left), Face::Left);
assert_eq!(Face::from_node_face(NodeFace::Right), Face::Right);
assert_eq!(Face::Top.to_node_face(), NodeFace::Top);
assert_eq!(Face::Bottom.to_node_face(), NodeFace::Bottom);
assert_eq!(Face::Left.to_node_face(), NodeFace::Left);
assert_eq!(Face::Right.to_node_face(), NodeFace::Right);
}
#[test]
fn routing_core_build_orthogonal_path_float_emits_axis_aligned_segments() {
let points = build_orthogonal_path_float(
FPoint::new(10.0, 10.0),
FPoint::new(80.0, 60.0),
Direction::TopDown,
&[],
);
assert!(points.windows(2).all(|seg| {
(seg[0].x - seg[1].x).abs() < f64::EPSILON || (seg[0].y - seg[1].y).abs() < f64::EPSILON
}));
}
#[test]
fn overflow_backward_channel_precedence_prefers_backward_channel_over_overflow_target_slot() {
assert_eq!(
fan_in_primary_face_capacity(Direction::TopDown),
4,
"TD/Bt primary capacity should remain in sync with contract"
);
assert_eq!(
fan_in_primary_face_capacity(Direction::LeftRight),
2,
"LR/RL primary capacity should remain in sync with contract"
);
assert_eq!(
canonical_backward_channel_face(Direction::TopDown),
Face::Right,
"TD backward canonical channel should be right"
);
assert_eq!(
canonical_backward_channel_face(Direction::LeftRight),
Face::Bottom,
"LR backward canonical channel should be bottom"
);
assert_eq!(
fan_in_overflow_face_for_slot(Direction::TopDown, OverflowSide::LeftOrTop),
Face::Left,
"TD overflow slot 0 should map to left"
);
assert_eq!(
fan_in_overflow_face_for_slot(Direction::TopDown, OverflowSide::RightOrBottom),
Face::Right,
"TD overflow slot 1 should map to right"
);
assert_eq!(
resolve_overflow_backward_channel_conflict(
Direction::TopDown,
true,
true,
Some(Face::Right),
Face::Right
),
Face::Right
);
assert_eq!(
resolve_overflow_backward_channel_conflict(
Direction::TopDown,
true,
true,
Some(Face::Top),
Face::Top
),
Face::Right
);
assert_eq!(
resolve_overflow_backward_channel_conflict(
Direction::TopDown,
false,
false,
Some(Face::Right),
Face::Right
),
Face::Right
);
}
#[test]
fn overflow_backward_channel_precedence_resolver_is_stable_and_direction_aware() {
let cases = [
(
Direction::TopDown,
true,
true,
Some(Face::Right),
Face::Right,
Face::Right,
),
(
Direction::TopDown,
false,
true,
Some(Face::Right),
Face::Right,
Face::Left,
),
(
Direction::TopDown,
false,
true,
Some(Face::Left),
Face::Left,
Face::Left,
),
(
Direction::LeftRight,
true,
true,
Some(Face::Bottom),
Face::Bottom,
Face::Bottom,
),
(
Direction::LeftRight,
false,
true,
Some(Face::Bottom),
Face::Bottom,
Face::Top,
),
(
Direction::LeftRight,
false,
false,
Some(Face::Bottom),
Face::Bottom,
Face::Bottom,
),
];
for (direction, is_backward, has_backward_conflict, overflow_face, proposed, expected) in cases
{
let first = resolve_overflow_backward_channel_conflict(
direction,
is_backward,
has_backward_conflict,
overflow_face,
proposed,
);
let second = resolve_overflow_backward_channel_conflict(
direction,
is_backward,
has_backward_conflict,
overflow_face,
proposed,
);
assert_eq!(
first, expected,
"resolver returned wrong face for direction={direction:?}, backward={is_backward}, conflict={has_backward_conflict}, overflow={overflow_face:?}, proposed={proposed:?}"
);
assert_eq!(
first, second,
"resolver must be deterministic across repeated evaluation"
);
}
}
#[test]
fn plan_attachments_spreads_edges_monotonically_on_same_face() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A"));
diagram.add_node(Node::new("B"));
diagram.add_node(Node::new("C"));
diagram.add_node(Node::new("D"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("A", "C"));
diagram.add_edge(Edge::new("A", "D"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(20, 3, 7, 3)),
("B", make_bounds_sized(5, 13, 7, 3)),
("C", make_bounds_sized(20, 13, 7, 3)),
("D", make_bounds_sized(35, 13, 7, 3)),
],
&[],
);
let plan = plan_attachments(&diagram.edges, &layout, Direction::TopDown);
let fractions = plan.source_fractions_for("A", Face::Bottom);
assert!(
fractions.windows(2).all(|w| w[0] <= w[1]),
"source fractions must be monotonic: {fractions:?}"
);
}
#[test]
fn plan_attachments_is_stable_for_equal_cross_axis_positions() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A"));
diagram.add_node(Node::new("B"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("A", "B"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(10, 3, 7, 3)),
("B", make_bounds_sized(10, 13, 7, 3)),
],
&[],
);
let first = plan_attachments(&diagram.edges, &layout, Direction::TopDown);
let second = plan_attachments(&diagram.edges, &layout, Direction::TopDown);
assert_eq!(first, second);
}
#[test]
fn shared_planner_adapter_spreads_fan_in_arrivals() {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("A"));
diagram.add_node(Node::new("B"));
diagram.add_node(Node::new("C"));
diagram.add_node(Node::new("D"));
diagram.add_node(Node::new("E"));
diagram.add_node(Node::new("F"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("A", "C"));
diagram.add_edge(Edge::new("A", "D"));
diagram.add_edge(Edge::new("A", "E"));
diagram.add_edge(Edge::new("A", "F"));
let layout = minimal_layout(
&[
("A", make_bounds_sized(30, 3, 7, 3)),
("B", make_bounds_sized(3, 13, 7, 3)),
("C", make_bounds_sized(15, 13, 7, 3)),
("D", make_bounds_sized(27, 13, 7, 3)),
("E", make_bounds_sized(39, 13, 7, 3)),
("F", make_bounds_sized(51, 13, 7, 3)),
],
&[],
);
let overrides =
compute_attachment_plan_from_shared_planner(&diagram.edges, &layout, Direction::TopDown);
let mut src_x: Vec<usize> = diagram
.edges
.iter()
.filter(|edge| edge.from == "A")
.filter_map(|edge| {
overrides
.get(&edge.index)
.and_then(|ov| ov.source.map(|p| p.0))
})
.collect();
src_x.sort_unstable();
src_x.dedup();
assert!(
src_x.len() > 1,
"fan-in/fan-out source attachments should be spread: {src_x:?}"
);
}
#[test]
fn compact_lr_backward_route_yields_to_subgraph_border_in_corridor() {
let mut diagram = Graph::new(Direction::LeftRight);
diagram.add_node(Node::new("A").with_label("Start"));
diagram.add_node(Node::new("B").with_label("End"));
diagram.add_edge(Edge::new("A", "B"));
diagram.add_edge(Edge::new("B", "A"));
let from = make_bounds_sized(20, 2, 8, 3);
let to = make_bounds_sized(4, 2, 8, 3);
let mut layout = minimal_layout(&[("A", to), ("B", from)], &[]);
layout.subgraph_bounds.insert(
"blocker".to_string(),
SubgraphBounds {
x: 14,
y: 1,
width: 5,
height: 6,
title: "Blocker".to_string(),
depth: 0,
invisible: false,
},
);
let result = route_edge_with_probe(
&diagram.edges[1],
&layout,
diagram.direction,
None,
None,
false,
)
.expect(
"compact LR backward edge should still route when a subgraph border blocks the direct lane",
);
assert_eq!(result.probe.path_family, TextPathFamily::SyntheticBackward);
}