#![cfg_attr(not(test), allow(dead_code))]
use std::collections::HashMap;
use super::label_util::{
calc_label_position, clamp_label_x, effective_edge_label, label_block, label_top_for_center,
};
use crate::graph::geometry::{EdgeLabelGeometry, EdgeLabelSide, RoutedGraphGeometry};
use crate::graph::grid::label_placement::{
CellRole, PathFootprint, choose_corridor_aware_anchor, claim_label_cells_into,
extend_segments_into, label_rect_overlaps_nodes, seed_node_cells_into,
seed_subgraph_borders_into,
};
use crate::graph::grid::{GridLayout, NodeBounds, RoutedEdge, Segment};
use crate::graph::{Edge, Stroke};
const OFF_CORRIDOR_DRIFT_THRESHOLD: f64 = 3.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTimePlacementScope {
AuthoritativeOnly,
AllBodyLabels,
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] pub(crate) struct RenderTimePlacement {
pub center: (usize, usize),
pub side: EdgeLabelSide,
pub is_backward: bool,
pub label_dims: (usize, usize),
}
#[derive(Debug, Clone, Copy)]
struct ClaimedLabel {
top_left_x: usize,
top_left_y: usize,
width: usize,
height: usize,
}
impl ClaimedLabel {
fn from_center(center: (usize, usize), dims: (usize, usize)) -> Self {
let (w, h) = (dims.0.max(1), dims.1.max(1));
Self {
top_left_x: center.0.saturating_sub(w / 2),
top_left_y: center.1.saturating_sub(h / 2),
width: w,
height: h,
}
}
fn overlaps(&self, top_left: (usize, usize), dims: (usize, usize)) -> bool {
let (w, h) = (dims.0.max(1), dims.1.max(1));
let self_end_x = self.top_left_x.saturating_add(self.width).saturating_add(1);
let self_start_x = self.top_left_x.saturating_sub(1);
let self_end_y = self.top_left_y.saturating_add(self.height);
let other_end_x = top_left.0.saturating_add(w).saturating_add(1);
let other_start_x = top_left.0.saturating_sub(1);
let other_end_y = top_left.1.saturating_add(h);
other_start_x < self_end_x
&& self_start_x < other_end_x
&& top_left.1 < self_end_y
&& self.top_left_y < other_end_y
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_label_placements(
routed_edges: &[RoutedEdge],
routed_geometry: Option<&RoutedGraphGeometry>,
layout: &GridLayout,
edge_containment: &HashMap<usize, (usize, usize)>,
canvas_width: usize,
canvas_height: usize,
scope: RenderTimePlacementScope,
) -> HashMap<usize, RenderTimePlacement> {
let mut placements: HashMap<usize, RenderTimePlacement> = HashMap::new();
let mut claimed: Vec<ClaimedLabel> = Vec::new();
let mut footprint = PathFootprint::default();
seed_subgraph_borders_into(&mut footprint, layout);
seed_node_cells_into(&mut footprint, layout);
for routed in routed_edges {
if routed.edge.stroke == Stroke::Invisible {
continue;
}
extend_segments_into(&routed.segments, &mut footprint);
}
for routed in routed_edges {
if routed.edge.stroke == Stroke::Invisible {
continue;
}
let Some(effective) = effective_edge_label(&routed.edge) else {
continue;
};
let label = effective.as_ref();
let block = label_block(label);
if block.width == 0 || block.height == 0 {
continue;
}
let label_dims = (block.width, block.height);
let geometry = routed_geometry.and_then(|rg| edge_label_geometry(rg, &routed.edge));
let is_authoritative = geometry.is_some_and(is_authoritative_geometry);
if matches!(scope, RenderTimePlacementScope::AuthoritativeOnly) && !is_authoritative {
continue;
}
let midpoint = calc_label_position(&routed.segments).map(|p| (p.x, p.y));
let projected = geometry.map(|g| layout.project_layout_point(g.center.x, g.center.y));
let side = geometry.map(|g| g.side).unwrap_or(EdgeLabelSide::Center);
let backward_midpoint = backward_midpoint_candidate(
routed,
midpoint,
side,
label_dims,
&footprint,
&claimed,
&layout.node_bounds,
canvas_width,
canvas_height,
);
let projected = if should_prefer_midpoint_for_forward_edge(
routed,
geometry,
projected,
midpoint,
label_dims,
&footprint,
&claimed,
&layout.node_bounds,
) {
None
} else {
projected
};
let Some(candidate_center) = backward_midpoint.or(projected).or(midpoint) else {
continue;
};
let corridor_anchor = choose_corridor_aware_anchor(
candidate_center,
side,
&footprint,
canvas_width,
canvas_height,
label_dims.0,
label_dims.1,
);
let node_safe_center =
if label_rect_overlaps_nodes(corridor_anchor, label_dims, &layout.node_bounds) {
let Some(m) = midpoint else { continue };
let retry = choose_corridor_aware_anchor(
m,
side,
&footprint,
canvas_width,
canvas_height,
label_dims.0,
label_dims.1,
);
if label_rect_overlaps_nodes(retry, label_dims, &layout.node_bounds) {
continue;
}
retry
} else {
corridor_anchor
};
let shifted_center = shift_against_claimed_labels(
node_safe_center,
label_dims,
&claimed,
canvas_width,
canvas_height,
);
let bounds = edge_containment.get(&routed.edge.index).copied();
let base_x = shifted_center.0.saturating_sub(label_dims.0 / 2);
let base_y = label_top_for_center(shifted_center.1, label_dims.1);
let final_x = clamp_label_x(base_x, label_dims.0, bounds);
let final_center = (
final_x.saturating_add(label_dims.0 / 2),
label_center_from_top(base_y, label_dims.1),
);
claim_label_cells_into(final_center, label_dims, &mut footprint);
claimed.push(ClaimedLabel::from_center(final_center, label_dims));
placements.insert(
routed.edge.index,
RenderTimePlacement {
center: final_center,
side,
is_backward: routed.is_backward,
label_dims,
},
);
}
placements
}
#[allow(clippy::too_many_arguments)]
fn backward_midpoint_candidate(
routed: &RoutedEdge,
midpoint: Option<(usize, usize)>,
side: EdgeLabelSide,
label_dims: (usize, usize),
footprint: &PathFootprint,
claimed: &[ClaimedLabel],
node_bounds: &HashMap<String, NodeBounds>,
canvas_width: usize,
canvas_height: usize,
) -> Option<(usize, usize)> {
if !routed.is_backward {
return None;
}
let midpoint = midpoint?;
if label_block_overlaps_claimed(midpoint, label_dims, claimed) {
return None;
}
if !label_block_hits_load_bearing_cell(midpoint, label_dims, footprint) {
return Some(midpoint);
}
if label_dims.1 > 1 {
return None;
}
let shifted = choose_corridor_aware_anchor(
midpoint,
side,
footprint,
canvas_width,
canvas_height,
label_dims.0,
label_dims.1,
);
if shifted == midpoint
|| shifted.0.abs_diff(midpoint.0) > 1
|| shifted.1.abs_diff(midpoint.1) > 1
|| label_block_overlaps_claimed(shifted, label_dims, claimed)
|| label_rect_overlaps_nodes(shifted, label_dims, node_bounds)
{
return None;
}
Some(shifted)
}
fn edge_label_geometry<'rg>(
routed: &'rg RoutedGraphGeometry,
edge: &Edge,
) -> Option<&'rg EdgeLabelGeometry> {
routed
.edges
.iter()
.find(|e| e.index == edge.index)
.and_then(|e| e.label_geometry.as_ref())
}
fn is_authoritative_geometry(geometry: &EdgeLabelGeometry) -> bool {
geometry.track != 0 || geometry.compartment_size > 1
}
#[allow(clippy::too_many_arguments)]
fn should_prefer_midpoint_for_forward_edge(
routed: &RoutedEdge,
geometry: Option<&EdgeLabelGeometry>,
projected: Option<(usize, usize)>,
midpoint: Option<(usize, usize)>,
label_dims: (usize, usize),
footprint: &PathFootprint,
claimed: &[ClaimedLabel],
node_bounds: &HashMap<String, NodeBounds>,
) -> bool {
if routed.is_backward {
return false;
}
let Some(geometry) = geometry else {
return false;
};
let (Some(projected), Some(midpoint)) = (projected, midpoint) else {
return false;
};
if distance_to_segments(projected, &routed.segments) <= OFF_CORRIDOR_DRIFT_THRESHOLD
|| distance_to_segments(midpoint, &routed.segments) > OFF_CORRIDOR_DRIFT_THRESHOLD
{
return false;
}
if !is_authoritative_geometry(geometry)
|| (geometry.compartment_size == 2 && geometry.track != 0)
{
return true;
}
geometry.compartment_size > 2
&& geometry.track != 0
&& label_dims.0 <= 2
&& label_dims.1 == 1
&& !label_block_hits_load_bearing_cell(midpoint, label_dims, footprint)
&& !label_block_overlaps_claimed(midpoint, label_dims, claimed)
&& !label_rect_overlaps_nodes(midpoint, label_dims, node_bounds)
}
fn distance_to_segments(point: (usize, usize), segments: &[Segment]) -> f64 {
let (px, py) = (point.0 as f64, point.1 as f64);
segments
.iter()
.map(|segment| match *segment {
Segment::Horizontal { y, x_start, x_end } => {
let (x_min, x_max) = (x_start.min(x_end) as f64, x_start.max(x_end) as f64);
let clamped_x = px.max(x_min).min(x_max);
((clamped_x - px).powi(2) + (y as f64 - py).powi(2)).sqrt()
}
Segment::Vertical { x, y_start, y_end } => {
let (y_min, y_max) = (y_start.min(y_end) as f64, y_start.max(y_end) as f64);
let clamped_y = py.max(y_min).min(y_max);
((x as f64 - px).powi(2) + (clamped_y - py).powi(2)).sqrt()
}
})
.fold(f64::INFINITY, f64::min)
}
fn label_center_from_top(top_y: usize, height: usize) -> usize {
top_y + height / 2
}
fn label_block_hits_load_bearing_cell(
center: (usize, usize),
dims: (usize, usize),
footprint: &PathFootprint,
) -> bool {
let base_x = center.0.saturating_sub(dims.0 / 2);
let base_y = center.1.saturating_sub(dims.1 / 2);
for row in base_y..base_y.saturating_add(dims.1.max(1)) {
for col in base_x..base_x.saturating_add(dims.0.max(1)) {
if matches!(
footprint.cells.get(&(col, row)),
Some(CellRole::Corner | CellRole::Terminal)
) {
return true;
}
}
}
false
}
fn label_block_overlaps_claimed(
center: (usize, usize),
dims: (usize, usize),
claimed: &[ClaimedLabel],
) -> bool {
let base_x = center.0.saturating_sub(dims.0 / 2);
let base_y = center.1.saturating_sub(dims.1 / 2);
claimed_overlaps(claimed, (base_x, base_y), dims)
}
fn shift_against_claimed_labels(
center: (usize, usize),
dims: (usize, usize),
claimed: &[ClaimedLabel],
canvas_width: usize,
canvas_height: usize,
) -> (usize, usize) {
let base_x = center.0.saturating_sub(dims.0 / 2);
let base_y = center.1.saturating_sub(dims.1 / 2);
if !claimed_overlaps(claimed, (base_x, base_y), dims) {
return center;
}
const SHIFTS: &[(isize, isize)] = &[
(0, -1),
(0, 1),
(0, -2),
(0, 2),
(-1, 0),
(1, 0),
(-2, 0),
(2, 0),
(0, -3),
(0, 3),
(-3, 0),
(3, 0),
];
for (dx, dy) in SHIFTS {
let Some(new_x) = offset_clamped(center.0, *dx, canvas_width) else {
continue;
};
let Some(new_y) = offset_clamped(center.1, *dy, canvas_height) else {
continue;
};
let nbx = new_x.saturating_sub(dims.0 / 2);
let nby = new_y.saturating_sub(dims.1 / 2);
if !claimed_overlaps(claimed, (nbx, nby), dims) {
return (new_x, new_y);
}
}
center
}
fn claimed_overlaps(
claimed: &[ClaimedLabel],
top_left: (usize, usize),
dims: (usize, usize),
) -> bool {
claimed.iter().any(|c| c.overlaps(top_left, dims))
}
fn offset_clamped(value: usize, delta: isize, max_exclusive: usize) -> Option<usize> {
let signed = value as isize + delta;
if signed < 0 {
return None;
}
let unsigned = signed as usize;
if unsigned >= max_exclusive {
return None;
}
Some(unsigned)
}