#![allow(dead_code)]
use crate::graph::edge::{Arrow, Edge};
use crate::graph::edge_marker::marker_avoidance_distance;
use crate::graph::geometry::{RoutedEdgeGeometry, RoutedGraphGeometry};
use crate::graph::measure::ProportionalTextMetrics;
use crate::graph::{Direction, Graph};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Axis {
Y,
X,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct LabelGapMeasurement {
pub gap: f64,
pub span: f64,
pub axis: Axis,
}
pub(crate) fn resolve_visual_endpoints<'a>(
edge: &'a RoutedEdgeGeometry,
diagram_edge: &'a Edge,
) -> (&'a str, &'a str, Arrow, Arrow) {
let _ = edge.is_backward;
(
edge.from.as_str(),
edge.to.as_str(),
diagram_edge.arrow_start,
diagram_edge.arrow_end,
)
}
pub(crate) fn compute_label_gap_and_span(
edge: &RoutedEdgeGeometry,
routed: &RoutedGraphGeometry,
diagram: &Graph,
direction: Direction,
metrics: &ProportionalTextMetrics,
) -> Option<LabelGapMeasurement> {
let geom = edge.label_geometry.as_ref()?;
let diagram_edge = diagram.edges.get(edge.index)?;
let from_rect = routed.nodes.get(edge.from.as_str()).map(|n| n.rect)?;
let to_rect = routed.nodes.get(edge.to.as_str()).map(|n| n.rect)?;
let s = edge_label_spacing(metrics);
let from_avoid = marker_avoidance_distance(diagram_edge.arrow_start);
let to_avoid = marker_avoidance_distance(diagram_edge.arrow_end);
let (axis, gap, span) = match direction {
Direction::TopDown | Direction::BottomTop => {
let (upper, upper_avoid, lower, lower_avoid) = if from_rect.y <= to_rect.y {
(from_rect, from_avoid, to_rect, to_avoid)
} else {
(to_rect, to_avoid, from_rect, from_avoid)
};
let gap = lower.y - (upper.y + upper.height) - upper_avoid - lower_avoid - 2.0 * s;
(Axis::Y, gap, geom.rect.height)
}
Direction::LeftRight | Direction::RightLeft => {
let (left, left_avoid, right, right_avoid) = if from_rect.x <= to_rect.x {
(from_rect, from_avoid, to_rect, to_avoid)
} else {
(to_rect, to_avoid, from_rect, from_avoid)
};
let gap = right.x - (left.x + left.width) - left_avoid - right_avoid - 2.0 * s;
(Axis::X, gap, geom.rect.width)
}
};
Some(LabelGapMeasurement { gap, span, axis })
}
fn edge_label_spacing(metrics: &ProportionalTextMetrics) -> f64 {
metrics.label_padding_y
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::graph::geometry::{
EdgeLabelGeometry, EdgeLabelSide, FPoint, FRect, PositionedNode, RoutedGraphGeometry,
};
use crate::graph::measure::default_proportional_text_metrics;
use crate::graph::{Edge, Graph, Node};
fn synthetic_routed_td(
source_rect: FRect,
target_rect: FRect,
label_rect: FRect,
is_backward: bool,
) -> (Graph, RoutedGraphGeometry) {
let mut diagram = Graph::new(Direction::TopDown);
diagram.add_node(Node::new("S"));
diagram.add_node(Node::new("T"));
diagram.add_edge(Edge::new("S", "T").with_label("label"));
let mut nodes = HashMap::new();
nodes.insert(
"S".into(),
PositionedNode {
id: "S".into(),
rect: source_rect,
shape: crate::graph::Shape::Rectangle,
label: "S".into(),
parent: None,
},
);
nodes.insert(
"T".into(),
PositionedNode {
id: "T".into(),
rect: target_rect,
shape: crate::graph::Shape::Rectangle,
label: "T".into(),
parent: None,
},
);
let edge = RoutedEdgeGeometry {
index: 0,
from: "S".into(),
to: "T".into(),
path: vec![
FPoint::new(source_rect.center_x(), source_rect.y + source_rect.height),
FPoint::new(target_rect.center_x(), target_rect.y),
],
label_position: Some(FPoint::new(
label_rect.x + label_rect.width / 2.0,
label_rect.y + label_rect.height / 2.0,
)),
label_side: Some(EdgeLabelSide::Above),
head_label_position: None,
tail_label_position: None,
is_backward,
from_subgraph: None,
to_subgraph: None,
source_port: None,
target_port: None,
preserve_orthogonal_topology: false,
label_geometry: Some(EdgeLabelGeometry {
center: FPoint::new(
label_rect.x + label_rect.width / 2.0,
label_rect.y + label_rect.height / 2.0,
),
rect: label_rect,
padding: (4.0, 2.0),
side: EdgeLabelSide::Above,
track: 0,
compartment_size: 1,
}),
effective_wrapped_lines: None,
};
let routed = RoutedGraphGeometry {
nodes,
edges: vec![edge],
subgraphs: HashMap::new(),
self_edges: vec![],
direction: Direction::TopDown,
bounds: FRect::new(0.0, 0.0, 200.0, 200.0),
unfit_label_overlaps: Vec::new(),
};
(diagram, routed)
}
#[test]
fn forward_edge_resolves_to_authored_endpoints() {
let (diagram, routed) = synthetic_routed_td(
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(0.0, 100.0, 50.0, 30.0),
FRect::new(10.0, 50.0, 30.0, 20.0),
false,
);
let (vs, vt, sa, ta) = resolve_visual_endpoints(&routed.edges[0], &diagram.edges[0]);
assert_eq!(vs, "S");
assert_eq!(vt, "T");
assert_eq!(sa, Arrow::None); assert_eq!(ta, Arrow::Normal); }
#[test]
fn resolve_visual_endpoints_is_identity_regardless_of_backward_or_direction() {
for is_backward in [false, true] {
let (diagram, routed) = synthetic_routed_td(
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(0.0, 100.0, 50.0, 30.0),
FRect::new(10.0, 50.0, 30.0, 20.0),
is_backward,
);
let (vs, vt, sa, ta) = resolve_visual_endpoints(&routed.edges[0], &diagram.edges[0]);
assert_eq!(vs, "S", "is_backward={is_backward}: from preserved");
assert_eq!(vt, "T", "is_backward={is_backward}: to preserved");
assert_eq!(sa, Arrow::None, "arrow_start preserved");
assert_eq!(ta, Arrow::Normal, "arrow_end preserved");
}
}
#[test]
fn bt_gap_uses_rect_positions_not_authored_direction() {
let (diagram, routed) = synthetic_routed_td(
FRect::new(0.0, 100.0, 50.0, 30.0),
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(10.0, 50.0, 30.0, 20.0),
false,
);
let metrics = default_proportional_text_metrics();
let m = compute_label_gap_and_span(
&routed.edges[0],
&routed,
&diagram,
Direction::BottomTop,
&metrics,
)
.expect("measurement should be Some");
assert_eq!(m.axis, Axis::Y);
assert!(
(m.gap - 58.0).abs() < 0.01,
"BT gap: expected 58, got {}",
m.gap
);
assert!(m.gap > 0.0, "BT gap must be positive for healthy edges");
}
#[test]
fn rl_gap_uses_rect_positions_not_authored_direction() {
let (diagram, routed) = synthetic_routed_td(
FRect::new(100.0, 0.0, 50.0, 30.0),
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(60.0, 5.0, 25.0, 20.0),
false,
);
let metrics = default_proportional_text_metrics();
let m = compute_label_gap_and_span(
&routed.edges[0],
&routed,
&diagram,
Direction::RightLeft,
&metrics,
)
.expect("measurement should be Some");
assert_eq!(m.axis, Axis::X);
assert!(
(m.gap - 38.0).abs() < 0.01,
"RL gap: expected 38, got {}",
m.gap
);
assert!(m.gap > 0.0, "RL gap must be positive for healthy edges");
}
#[test]
fn td_gap_and_span_with_no_markers() {
let (diagram, routed) = synthetic_routed_td(
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(0.0, 100.0, 50.0, 30.0),
FRect::new(10.0, 50.0, 30.0, 20.0),
false,
);
let metrics = default_proportional_text_metrics();
let m = compute_label_gap_and_span(
&routed.edges[0],
&routed,
&diagram,
Direction::TopDown,
&metrics,
)
.expect("measurement should be Some");
assert_eq!(m.axis, Axis::Y);
assert!(
(m.gap - 58.0).abs() < 0.01,
"expected gap=58, got {}",
m.gap
);
assert_eq!(m.span, 20.0);
}
#[test]
fn unfit_when_label_taller_than_gap() {
let (diagram, routed) = synthetic_routed_td(
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(0.0, 50.0, 50.0, 30.0), FRect::new(10.0, 30.0, 30.0, 50.0), false,
);
let metrics = default_proportional_text_metrics();
let m = compute_label_gap_and_span(
&routed.edges[0],
&routed,
&diagram,
Direction::TopDown,
&metrics,
)
.expect("measurement should be Some");
assert!(
m.gap < m.span,
"expected unfit (gap={} < span={})",
m.gap,
m.span
);
}
#[test]
fn no_label_geometry_returns_none() {
let (diagram, mut routed) = synthetic_routed_td(
FRect::new(0.0, 0.0, 50.0, 30.0),
FRect::new(0.0, 100.0, 50.0, 30.0),
FRect::new(10.0, 50.0, 30.0, 20.0),
false,
);
routed.edges[0].label_geometry = None;
let metrics = default_proportional_text_metrics();
assert!(
compute_label_gap_and_span(
&routed.edges[0],
&routed,
&diagram,
Direction::TopDown,
&metrics,
)
.is_none()
);
}
}