pub(crate) mod svg;
pub mod text;
pub use self::svg::SvgRenderOptions;
use crate::format::{OutputFormat, RoutingStyle, TextColorMode};
use crate::graph::direction_policy::build_node_directions;
use crate::graph::geometry::{GraphGeometry, LayoutEdge, RoutedGraphGeometry, SelfEdgeGeometry};
use crate::graph::routing::{self, EdgeRouting};
use crate::graph::{Direction, Graph};
use crate::render::svg::theme::ResolvedSvgTheme;
use crate::simplification::PathSimplification;
pub(crate) fn edge_routing_from_style(routing_style: RoutingStyle) -> EdgeRouting {
match routing_style {
RoutingStyle::Direct => EdgeRouting::DirectRoute,
RoutingStyle::Polyline => EdgeRouting::PolylineRoute,
RoutingStyle::Orthogonal => EdgeRouting::OrthogonalRoute,
}
}
#[derive(Debug, Clone)]
pub struct TextRenderOptions {
pub output_format: OutputFormat,
pub text_color_mode: TextColorMode,
pub routing_style: RoutingStyle,
pub cluster_ranksep: Option<f64>,
pub padding: Option<usize>,
#[allow(dead_code)]
pub path_simplification: PathSimplification,
}
impl Default for TextRenderOptions {
fn default() -> Self {
Self {
output_format: OutputFormat::Text,
text_color_mode: TextColorMode::Plain,
routing_style: RoutingStyle::Orthogonal,
cluster_ranksep: None,
padding: None,
path_simplification: PathSimplification::default(),
}
}
}
#[cfg(test)]
pub(crate) fn render_svg_from_geometry(
diagram: &Graph,
geometry: &GraphGeometry,
options: &SvgRenderOptions,
) -> String {
render_svg_from_geometry_with_routing(
diagram,
geometry,
options,
edge_routing_from_style(options.routing_style),
)
}
#[cfg(test)]
pub(crate) fn render_svg_from_routed_geometry(
diagram: &Graph,
routed: &RoutedGraphGeometry,
options: &SvgRenderOptions,
) -> String {
let geometry = geometry_for_routed_svg(diagram, routed);
render_svg_from_geometry_with_routing(diagram, &geometry, options, EdgeRouting::EngineProvided)
}
#[cfg(test)]
pub(crate) fn render_svg_from_geometry_with_routing(
diagram: &Graph,
geometry: &GraphGeometry,
options: &SvgRenderOptions,
edge_routing: EdgeRouting,
) -> String {
render_svg_from_geometry_with_theme_and_routing(diagram, geometry, options, edge_routing, None)
}
pub(crate) fn render_svg_from_geometry_with_theme_and_routing(
diagram: &Graph,
geometry: &GraphGeometry,
options: &SvgRenderOptions,
edge_routing: EdgeRouting,
theme: Option<&ResolvedSvgTheme>,
) -> String {
svg::render_svg_from_geometry_with_theme(diagram, options, geometry, edge_routing, theme)
}
pub(crate) fn render_svg_from_routed_geometry_with_theme(
diagram: &Graph,
routed: &RoutedGraphGeometry,
options: &SvgRenderOptions,
theme: Option<&ResolvedSvgTheme>,
) -> String {
let geometry = geometry_for_routed_svg(diagram, routed);
render_svg_from_geometry_with_theme_and_routing(
diagram,
&geometry,
options,
EdgeRouting::EngineProvided,
theme,
)
}
fn geometry_for_routed_svg(diagram: &Graph, routed: &RoutedGraphGeometry) -> GraphGeometry {
GraphGeometry {
nodes: routed.nodes.clone(),
edges: routed
.edges
.iter()
.map(|edge| LayoutEdge {
index: edge.index,
from: edge.from.clone(),
to: edge.to.clone(),
waypoints: vec![],
label_position: edge.label_position,
label_side: edge.label_side,
from_subgraph: edge.from_subgraph.clone(),
to_subgraph: edge.to_subgraph.clone(),
layout_path_hint: Some(edge.path.clone()),
preserve_orthogonal_topology: edge.preserve_orthogonal_topology,
})
.collect(),
subgraphs: routed.subgraphs.clone(),
self_edges: routed
.self_edges
.iter()
.map(|edge| SelfEdgeGeometry {
node_id: edge.node_id.clone(),
edge_index: edge.edge_index,
points: edge.path.clone(),
})
.collect(),
direction: routed.direction,
node_directions: build_node_directions(diagram),
bounds: routed.bounds,
reversed_edges: routed
.edges
.iter()
.filter(|edge| edge.is_backward)
.map(|edge| edge.index)
.collect(),
engine_hints: None,
grid_projection: None,
rerouted_edges: std::collections::HashSet::new(),
enhanced_backward_routing: false,
}
}
pub fn render_text_from_geometry(
diagram: &Graph,
geometry: &GraphGeometry,
routed: Option<&RoutedGraphGeometry>,
options: &TextRenderOptions,
) -> String {
let routed_owned;
let routed = match routed {
Some(routed) => routed,
None => {
routed_owned = routing::route_graph_geometry(
diagram,
geometry,
edge_routing_from_style(options.routing_style),
);
&routed_owned
}
};
let config = layout_config_for_diagram(diagram, options);
let layout = crate::graph::grid::geometry_to_grid_layout_with_routed(
diagram,
geometry,
Some(routed),
&config,
);
text::render_text_from_grid_layout(diagram, &layout, options)
}
pub(crate) fn layout_config_for_diagram(
diagram: &Graph,
options: &TextRenderOptions,
) -> crate::graph::grid::GridLayoutConfig {
let mut config = crate::graph::grid::GridLayoutConfig::default();
let max_label_len = diagram
.edges
.iter()
.filter_map(|e| e.label.as_ref())
.map(|label| {
label
.split('\n')
.map(|line| line.chars().count())
.max()
.unwrap_or(0)
})
.max()
.unwrap_or(0);
match diagram.direction {
Direction::LeftRight | Direction::RightLeft => {
config.h_spacing = config.h_spacing.max(max_label_len + 4);
}
Direction::TopDown | Direction::BottomTop => {
if max_label_len > 0 {
let (has_branching, left_len, right_len) = branching_label_info(diagram);
if has_branching {
config.v_spacing = config.v_spacing.max(5);
config.h_spacing = config.h_spacing.max(left_len.max(right_len) + 4);
config.left_label_margin = left_len;
config.right_label_margin = right_len;
} else {
config.v_spacing = config.v_spacing.max(3);
}
}
}
}
if diagram.has_subgraphs() {
let max_depth = diagram
.subgraphs
.keys()
.map(|id| diagram.subgraph_depth(id))
.max()
.unwrap_or(0);
if max_depth > 0 {
config.padding += max_depth * 2;
}
}
if let Some(cluster_ranksep) = options.cluster_ranksep {
config.cluster_rank_sep = cluster_ranksep;
}
if let Some(padding) = options.padding {
config.padding = padding;
}
config
}
fn branching_label_info(diagram: &Graph) -> (bool, usize, usize) {
let mut labeled_edges_per_source: std::collections::HashMap<&str, Vec<&str>> =
std::collections::HashMap::new();
for edge in &diagram.edges {
if let Some(ref label) = edge.label {
labeled_edges_per_source
.entry(&edge.from)
.or_default()
.push(label);
}
}
let mut has_branching = false;
let mut max_left = 0;
let mut max_right = 0;
for labels in labeled_edges_per_source.values() {
if labels.len() >= 2 {
has_branching = true;
max_left = max_left.max(labels[0].chars().count());
max_right = max_right.max(
labels[1..]
.iter()
.map(|l| l.chars().count())
.max()
.unwrap_or(0),
);
}
}
(has_branching, max_left, max_right)
}