use std::collections::{HashMap, HashSet};
use super::kernel::{LayoutResult, NodeId, Point, Rect};
use crate::graph::attachment::{
AttachmentCandidate, AttachmentSide, Face, edge_faces as shared_edge_faces,
plan_attachment_candidates, point_on_face_float as shared_point_on_face_float,
};
use crate::graph::direction_policy::{
build_override_node_map, cross_boundary_edge_direction, effective_edge_direction,
};
use crate::graph::routing::build_orthogonal_path_float;
use crate::graph::space::FRect;
use crate::graph::{Direction, Graph};
fn exit_face(direction: Direction) -> Face {
shared_edge_faces(direction, false).0
}
fn entry_face(direction: Direction) -> Face {
shared_edge_faces(direction, false).1
}
fn point_on_face(rect: &Rect, face: Face, fraction: f64) -> Point {
let rect = FRect::from(*rect);
shared_point_on_face_float(rect, face, fraction).into()
}
#[cfg(test)]
fn exit_point(rect: &Rect, direction: Direction) -> Point {
point_on_face(rect, exit_face(direction), 0.5)
}
#[cfg(test)]
fn entry_point(rect: &Rect, direction: Direction) -> Point {
point_on_face(rect, entry_face(direction), 0.5)
}
#[cfg(test)]
pub fn route_float_edge_direct(
from_rect: &Rect,
to_rect: &Rect,
direction: Direction,
) -> Vec<Point> {
let start = exit_point(from_rect, direction);
let end = entry_point(to_rect, direction);
build_orthogonal_path_float(start.into(), end.into(), direction, &[])
.into_iter()
.map(Into::into)
.collect()
}
fn route_float_edge_ported(
from_rect: &Rect,
to_rect: &Rect,
direction: Direction,
from_port: f64,
to_port: f64,
) -> Vec<Point> {
let start = point_on_face(from_rect, exit_face(direction), from_port);
let end = point_on_face(to_rect, entry_face(direction), to_port);
build_orthogonal_path_float(start.into(), end.into(), direction, &[])
.into_iter()
.map(Into::into)
.collect()
}
fn float_spread_fraction_from_plan(fraction: f64, group_size: usize) -> f64 {
if group_size <= 1 {
0.5
} else {
let margin = 0.25; margin + (1.0 - 2.0 * margin) * fraction
}
}
#[cfg(test)]
pub fn route_float_edge_with_boundary(
from_rect: &Rect,
to_rect: &Rect,
_sg_rect: &Rect,
_from_is_inside: bool,
outside_direction: Direction,
) -> Vec<Point> {
route_float_edge_direct(from_rect, to_rect, outside_direction)
}
#[derive(Debug, Default)]
pub struct RerouteStats {
pub unaffected: usize,
pub internal: usize,
pub cross_boundary: usize,
}
pub fn reroute_override_edges(
diagram: &Graph,
layout: &mut LayoutResult,
node_directions: &HashMap<String, Direction>,
) -> (RerouteStats, HashSet<usize>) {
let has_overrides = diagram.subgraphs.values().any(|sg| sg.dir.is_some());
if !has_overrides {
return (RerouteStats::default(), HashSet::new());
}
let override_nodes = build_override_node_map(diagram);
let mut stats = RerouteStats::default();
let mut rerouted_indices = HashSet::new();
struct PendingRoute {
layout_pos: usize, edge_index: usize, direction: Direction,
from_id: String,
to_id: String,
}
let mut pending: Vec<PendingRoute> = Vec::new();
for (pos, edge_layout) in layout.edges.iter().enumerate() {
let Some(edge) = diagram.edges.get(edge_layout.index) else {
continue;
};
if edge.from_subgraph.is_some() || edge.to_subgraph.is_some() {
stats.unaffected += 1;
continue;
}
let from_sg = override_nodes.get(&edge.from);
let to_sg = override_nodes.get(&edge.to);
match (from_sg, to_sg) {
(None, None) => {
stats.unaffected += 1;
}
(Some(sg_a), Some(sg_b)) if sg_a == sg_b => {
stats.internal += 1;
let dir = effective_edge_direction(
node_directions,
&edge.from,
&edge.to,
diagram.direction,
);
pending.push(PendingRoute {
layout_pos: pos,
edge_index: edge_layout.index,
direction: dir,
from_id: edge.from.clone(),
to_id: edge.to.clone(),
});
}
_ => {
stats.cross_boundary += 1;
let outside_dir = cross_boundary_edge_direction(
diagram,
node_directions,
from_sg,
to_sg,
&edge.from,
&edge.to,
diagram.direction,
);
pending.push(PendingRoute {
layout_pos: pos,
edge_index: edge_layout.index,
direction: outside_dir,
from_id: edge.from.clone(),
to_id: edge.to.clone(),
});
}
}
}
let mut planner_inputs: Vec<AttachmentCandidate> = Vec::with_capacity(pending.len() * 2);
for (pi, pr) in pending.iter().enumerate() {
let from_rect = layout.nodes.get(&NodeId(pr.from_id.clone()));
let to_rect = layout.nodes.get(&NodeId(pr.to_id.clone()));
if let (Some(fr), Some(tr)) = (from_rect, to_rect) {
let horizontal_face = matches!(pr.direction, Direction::TopDown | Direction::BottomTop);
let exit_sort = if horizontal_face {
tr.x + tr.width / 2.0
} else {
tr.y + tr.height / 2.0
};
let entry_sort = if horizontal_face {
fr.x + fr.width / 2.0
} else {
fr.y + fr.height / 2.0
};
planner_inputs.push(AttachmentCandidate {
edge_index: pi,
node_id: pr.from_id.clone(),
side: AttachmentSide::Source,
face: exit_face(pr.direction),
cross_axis: exit_sort,
});
planner_inputs.push(AttachmentCandidate {
edge_index: pi,
node_id: pr.to_id.clone(),
side: AttachmentSide::Target,
face: entry_face(pr.direction),
cross_axis: entry_sort,
});
}
}
let attachment_plan = plan_attachment_candidates(planner_inputs);
let mut from_fractions: Vec<f64> = vec![0.5; pending.len()];
let mut to_fractions: Vec<f64> = vec![0.5; pending.len()];
for (pi, pr) in pending.iter().enumerate() {
if let Some(edge_plan) = attachment_plan.edge(pi) {
if let Some(source) = edge_plan.source {
from_fractions[pi] = float_spread_fraction_from_plan(
source.fraction,
attachment_plan.group_size(&pr.from_id, source.face),
);
}
if let Some(target) = edge_plan.target {
to_fractions[pi] = float_spread_fraction_from_plan(
target.fraction,
attachment_plan.group_size(&pr.to_id, target.face),
);
}
}
}
for (pi, pr) in pending.iter().enumerate() {
if let (Some(from_rect), Some(to_rect)) = (
layout.nodes.get(&NodeId(pr.from_id.clone())),
layout.nodes.get(&NodeId(pr.to_id.clone())),
) {
layout.edges[pr.layout_pos].points = route_float_edge_ported(
from_rect,
to_rect,
pr.direction,
from_fractions[pi],
to_fractions[pi],
);
rerouted_indices.insert(pr.edge_index);
}
}
(stats, rerouted_indices)
}
pub fn ensure_cross_boundary_edge_spacing(
diagram: &Graph,
layout: &mut LayoutResult,
node_directions: &HashMap<String, Direction>,
min_gap: f64,
) {
let has_overrides = diagram.subgraphs.values().any(|sg| sg.dir.is_some());
if !has_overrides {
return;
}
let override_nodes = build_override_node_map(diagram);
for edge in &diagram.edges {
if edge.from_subgraph.is_some() || edge.to_subgraph.is_some() {
continue; }
let from_sg = override_nodes.get(&edge.from);
let to_sg = override_nodes.get(&edge.to);
let is_cross = match (from_sg, to_sg) {
(Some(a), Some(b)) => a != b,
(Some(_), None) | (None, Some(_)) => true,
(None, None) => false,
};
if !is_cross {
continue;
}
let direction = cross_boundary_edge_direction(
diagram,
node_directions,
from_sg,
to_sg,
&edge.from,
&edge.to,
diagram.direction,
);
let from_key = NodeId(edge.from.clone());
let to_key = NodeId(edge.to.clone());
let from_rect = match layout.nodes.get(&from_key) {
Some(r) => *r,
None => continue,
};
let to_rect = match layout.nodes.get(&to_key) {
Some(r) => *r,
None => continue,
};
let gap = match direction {
Direction::TopDown => to_rect.y - (from_rect.y + from_rect.height),
Direction::BottomTop => from_rect.y - (to_rect.y + to_rect.height),
Direction::LeftRight => to_rect.x - (from_rect.x + from_rect.width),
Direction::RightLeft => from_rect.x - (to_rect.x + to_rect.width),
};
if gap < 0.0 || gap >= min_gap {
continue;
}
let shift = min_gap - gap;
let from_depth = from_sg.map(|sg| diagram.subgraph_depth(sg)).unwrap_or(0);
let to_depth = to_sg.map(|sg| diagram.subgraph_depth(sg)).unwrap_or(0);
let push_source = from_depth <= to_depth;
if push_source {
let r = layout.nodes.get_mut(&from_key).unwrap();
match direction {
Direction::TopDown => r.y -= shift,
Direction::BottomTop => r.y += shift,
Direction::LeftRight => r.x -= shift,
Direction::RightLeft => r.x += shift,
}
} else {
let r = layout.nodes.get_mut(&to_key).unwrap();
match direction {
Direction::TopDown => r.y += shift,
Direction::BottomTop => r.y -= shift,
Direction::LeftRight => r.x += shift,
Direction::RightLeft => r.x -= shift,
}
}
}
}
pub fn reroute_subgraph_node_edges(diagram: &Graph, layout: &mut LayoutResult) -> HashSet<usize> {
struct PendingRoute {
layout_pos: usize,
edge_index: usize,
direction: Direction,
from_id: String,
to_id: String,
}
let mut pending: Vec<PendingRoute> = Vec::new();
for (pos, edge_layout) in layout.edges.iter().enumerate() {
let Some(edge) = diagram.edges.get(edge_layout.index) else {
continue;
};
if edge.from_subgraph.is_none() && edge.to_subgraph.is_none() {
continue;
}
let from_id = edge
.from_subgraph
.as_ref()
.cloned()
.unwrap_or_else(|| edge.from.clone());
let to_id = edge
.to_subgraph
.as_ref()
.cloned()
.unwrap_or_else(|| edge.to.clone());
pending.push(PendingRoute {
layout_pos: pos,
edge_index: edge_layout.index,
direction: diagram.direction,
from_id,
to_id,
});
}
if pending.is_empty() {
return HashSet::new();
}
let mut face_edges: HashMap<(String, Face), Vec<(usize, f64)>> = HashMap::new();
for (pi, pr) in pending.iter().enumerate() {
let from_rect = get_rect(layout, &pr.from_id);
let to_rect = get_rect(layout, &pr.to_id);
if let (Some(fr), Some(tr)) = (from_rect, to_rect) {
let horizontal_face = matches!(pr.direction, Direction::TopDown | Direction::BottomTop);
let ef = exit_face(pr.direction);
let exit_sort = if horizontal_face {
tr.x + tr.width / 2.0
} else {
tr.y + tr.height / 2.0
};
face_edges
.entry((pr.from_id.clone(), ef))
.or_default()
.push((pi, exit_sort));
let nf = entry_face(pr.direction);
let entry_sort = if horizontal_face {
fr.x + fr.width / 2.0
} else {
fr.y + fr.height / 2.0
};
face_edges
.entry((pr.to_id.clone(), nf))
.or_default()
.push((pi, entry_sort));
}
}
let mut from_fractions: Vec<f64> = vec![0.5; pending.len()];
let mut to_fractions: Vec<f64> = vec![0.5; pending.len()];
for ((node_id, face), mut entries) in face_edges {
if entries.len() <= 1 {
continue;
}
entries.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let n = entries.len();
let margin = 0.25;
for (rank, &(pi, _)) in entries.iter().enumerate() {
let frac = margin + (1.0 - 2.0 * margin) * (rank as f64) / ((n - 1) as f64);
let pr = &pending[pi];
let is_exit = pr.from_id == node_id && exit_face(pr.direction) == face;
if is_exit {
from_fractions[pi] = frac;
} else {
to_fractions[pi] = frac;
}
}
}
let mut rerouted = HashSet::new();
for (pi, pr) in pending.iter().enumerate() {
let from_rect = get_rect(layout, &pr.from_id);
let to_rect = get_rect(layout, &pr.to_id);
if let (Some(fr), Some(tr)) = (from_rect, to_rect) {
layout.edges[pr.layout_pos].points =
route_float_edge_ported(fr, tr, pr.direction, from_fractions[pi], to_fractions[pi]);
rerouted.insert(pr.edge_index);
}
}
rerouted
}
fn get_rect<'a>(layout: &'a LayoutResult, id: &str) -> Option<&'a Rect> {
layout
.subgraph_bounds
.get(id)
.or_else(|| layout.nodes.get(&NodeId(id.to_string())))
}
pub fn align_cross_boundary_siblings(diagram: &Graph, layout: &mut LayoutResult) {
for (sg_id, sg) in &diagram.subgraphs {
let Some(sub_dir) = sg.dir else { continue };
let is_horizontal = matches!(sub_dir, Direction::LeftRight | Direction::RightLeft);
let child_sg_nodes: HashSet<&str> = diagram
.subgraphs
.iter()
.filter(|(_, child)| child.parent.as_deref() == Some(sg_id.as_str()))
.flat_map(|(_, child)| child.nodes.iter().map(|s| s.as_str()))
.collect();
if child_sg_nodes.is_empty() {
continue;
}
let direct_nodes: Vec<&str> = sg
.nodes
.iter()
.filter(|n| !diagram.is_subgraph(n) && !child_sg_nodes.contains(n.as_str()))
.map(|s| s.as_str())
.collect();
for node_id in &direct_nodes {
let mut target_cross: Vec<f64> = Vec::new();
for edge in &diagram.edges {
let target = if edge.from == *node_id && child_sg_nodes.contains(edge.to.as_str()) {
Some(edge.to.as_str())
} else if edge.to == *node_id && child_sg_nodes.contains(edge.from.as_str()) {
Some(edge.from.as_str())
} else {
None
};
if let Some(tid) = target {
let id = NodeId(tid.to_string());
if let Some(r) = layout.nodes.get(&id) {
let center = if is_horizontal {
r.y + r.height / 2.0
} else {
r.x + r.width / 2.0
};
target_cross.push(center);
}
}
}
if target_cross.is_empty() {
continue;
}
let avg: f64 = target_cross.iter().sum::<f64>() / target_cross.len() as f64;
let id = NodeId(node_id.to_string());
let Some(rect) = layout.nodes.get_mut(&id) else {
continue;
};
if is_horizontal {
let cy = rect.y + rect.height / 2.0;
if (avg - cy).abs() < 0.5 {
continue;
}
rect.y = avg - rect.height / 2.0;
} else {
let cx = rect.x + rect.width / 2.0;
if (avg - cx).abs() < 0.5 {
continue;
}
rect.x = avg - rect.width / 2.0;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_effective_edge_direction_same_override() {
let mut dirs = HashMap::new();
dirs.insert("A".to_string(), Direction::LeftRight);
dirs.insert("B".to_string(), Direction::LeftRight);
dirs.insert("C".to_string(), Direction::TopDown);
assert_eq!(
effective_edge_direction(&dirs, "A", "B", Direction::TopDown),
Direction::LeftRight,
);
assert_eq!(
effective_edge_direction(&dirs, "A", "C", Direction::TopDown),
Direction::TopDown,
);
}
#[test]
fn test_route_float_edge_direct_aligned_td() {
let from = Rect {
x: 90.0,
y: 10.0,
width: 20.0,
height: 20.0,
};
let to = Rect {
x: 90.0,
y: 60.0,
width: 20.0,
height: 20.0,
};
let points = route_float_edge_direct(&from, &to, Direction::TopDown);
assert_eq!(points.len(), 2);
assert!((points[0].x - 100.0).abs() < 0.01);
assert!((points[0].y - 30.0).abs() < 0.01);
assert!((points[1].x - 100.0).abs() < 0.01);
assert!((points[1].y - 60.0).abs() < 0.01);
}
#[test]
fn test_route_float_edge_direct_aligned_lr() {
let from = Rect {
x: 10.0,
y: 90.0,
width: 20.0,
height: 20.0,
};
let to = Rect {
x: 60.0,
y: 90.0,
width: 20.0,
height: 20.0,
};
let points = route_float_edge_direct(&from, &to, Direction::LeftRight);
assert_eq!(points.len(), 2);
assert!((points[0].x - 30.0).abs() < 0.01);
assert!((points[0].y - 100.0).abs() < 0.01);
assert!((points[1].x - 60.0).abs() < 0.01);
assert!((points[1].y - 100.0).abs() < 0.01);
}
#[test]
fn test_route_float_edge_direct_offset_needs_elbow() {
let from = Rect {
x: 10.0,
y: 10.0,
width: 20.0,
height: 20.0,
};
let to = Rect {
x: 60.0,
y: 10.0,
width: 20.0,
height: 20.0,
};
let points = route_float_edge_direct(&from, &to, Direction::TopDown);
assert!(points.len() >= 3);
}
#[test]
fn test_route_float_edge_with_boundary_exit() {
let from = Rect {
x: 40.0,
y: 40.0,
width: 20.0,
height: 20.0,
};
let to = Rect {
x: 40.0,
y: 150.0,
width: 20.0,
height: 20.0,
};
let sg = Rect {
x: 10.0,
y: 10.0,
width: 100.0,
height: 100.0,
};
let points = route_float_edge_with_boundary(&from, &to, &sg, true, Direction::TopDown);
assert!(!points.is_empty());
for p in &points {
assert!(p.x.is_finite() && p.y.is_finite(), "point has NaN: {:?}", p);
}
}
#[test]
fn test_route_float_edge_ported_center_matches_direct() {
let from = Rect {
x: 10.0,
y: 10.0,
width: 40.0,
height: 20.0,
};
let to = Rect {
x: 80.0,
y: 10.0,
width: 40.0,
height: 20.0,
};
let direct = route_float_edge_direct(&from, &to, Direction::TopDown);
let ported = route_float_edge_ported(&from, &to, Direction::TopDown, 0.5, 0.5);
assert_eq!(direct.len(), ported.len());
for (d, p) in direct.iter().zip(ported.iter()) {
assert!((d.x - p.x).abs() < 0.01, "x mismatch: {} vs {}", d.x, p.x);
assert!((d.y - p.y).abs() < 0.01, "y mismatch: {} vs {}", d.y, p.y);
}
}
#[test]
fn test_route_float_edge_ported_spread_endpoints() {
let from = Rect {
x: 100.0,
y: 10.0,
width: 60.0,
height: 40.0,
};
let to = Rect {
x: 100.0,
y: 100.0,
width: 60.0,
height: 40.0,
};
let left = route_float_edge_ported(&from, &to, Direction::TopDown, 0.5, 0.25);
let right = route_float_edge_ported(&from, &to, Direction::TopDown, 0.5, 0.75);
assert!((left[0].x - 130.0).abs() < 0.01);
assert!((right[0].x - 130.0).abs() < 0.01);
let left_end = left.last().unwrap();
let right_end = right.last().unwrap();
assert!(
(left_end.x - 115.0).abs() < 0.01,
"left entry x={}",
left_end.x
); assert!(
(right_end.x - 145.0).abs() < 0.01,
"right entry x={}",
right_end.x
); assert!((left_end.y - 100.0).abs() < 0.01); assert!((right_end.y - 100.0).abs() < 0.01);
}
#[test]
fn test_point_on_face_fractions() {
let rect = Rect {
x: 50.0,
y: 100.0,
width: 80.0,
height: 40.0,
};
let top_left = point_on_face(&rect, Face::Top, 0.0);
assert!((top_left.x - 50.0).abs() < 0.01);
assert!((top_left.y - 100.0).abs() < 0.01);
let top_center = point_on_face(&rect, Face::Top, 0.5);
assert!((top_center.x - 90.0).abs() < 0.01);
let top_right = point_on_face(&rect, Face::Top, 1.0);
assert!((top_right.x - 130.0).abs() < 0.01);
let right_mid = point_on_face(&rect, Face::Right, 0.5);
assert!((right_mid.x - 130.0).abs() < 0.01);
assert!((right_mid.y - 120.0).abs() < 0.01);
}
#[test]
fn test_point_on_face_clamps_fraction_bounds() {
let rect = Rect {
x: 50.0,
y: 100.0,
width: 80.0,
height: 40.0,
};
let below_zero = point_on_face(&rect, Face::Top, -1.0);
assert!((below_zero.x - 50.0).abs() < 0.01);
assert!((below_zero.y - 100.0).abs() < 0.01);
let above_one = point_on_face(&rect, Face::Left, 2.0);
assert!((above_one.x - 50.0).abs() < 0.01);
assert!((above_one.y - 140.0).abs() < 0.01);
}
}