#![allow(dead_code)]
pub mod architecture;
pub mod band;
pub mod block;
pub mod c4;
pub mod chart;
pub mod class;
pub mod clusters;
pub mod edges;
pub mod er;
pub mod gantt;
pub mod gitgraph;
pub mod journey;
pub mod kanban;
pub mod labels;
pub mod mindmap;
pub mod orthogonal;
pub mod panel;
pub mod requirement;
pub mod sequence;
pub mod shapes;
pub mod state;
pub mod style;
pub mod svg;
pub mod theme;
pub mod timeline;
pub mod zenuml;
#[cfg(test)]
mod class_tests;
#[cfg(test)]
mod decoration_tests;
#[cfg(test)]
mod er_tests;
#[cfg(test)]
mod kinds_tests;
#[cfg(test)]
mod sequence_tests;
#[cfg(test)]
mod state_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod text_placement_tests;
use std::collections::{HashMap, HashSet};
use std::fmt;
use crate::preview::mermaid::flowchart::{
self, Direction, Flowchart, LinkStyleTarget, ParseError, Shape, Stroke,
};
use crate::preview::mermaid::layout::{
graph::{Graph, GraphOptions},
layout, EdgeLabel, LayoutOptions, NodeLabel, Point, RankDir,
};
use crate::preview::mermaid::text_metrics;
pub use edges::{Curve, Tip};
pub use labels::Label;
pub use orthogonal::Routing;
pub use panel::Panel;
#[allow(unused_imports)]
pub use shapes::{Glyph, Mark, Size};
pub use style::ShapeStyle;
pub use theme::Theme;
pub const MARGIN: f64 = 8.0;
pub const NODE_SEP: f64 = 50.0;
pub const RANK_SEP: f64 = 50.0;
pub const EDGE_SEP: f64 = 20.0;
const ORTHO_NODE_SEP: f64 = 24.0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderError {
Parse(ParseError),
StateParse(crate::preview::mermaid::state::ParseError),
ClassParse(crate::preview::mermaid::class::ParseError),
ErParse(crate::preview::mermaid::er::ParseError),
SequenceParse(crate::preview::mermaid::sequence::ParseError),
ChartParse(crate::preview::mermaid::chart::ParseError),
ChartHasNoExtent {
what: &'static str,
},
NoFonts,
NothingToDraw,
}
impl fmt::Display for RenderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RenderError::Parse(e) => write!(f, "{e}"),
RenderError::StateParse(e) => write!(f, "{e}"),
RenderError::ClassParse(e) => write!(f, "{e}"),
RenderError::ErParse(e) => write!(f, "{e}"),
RenderError::SequenceParse(e) => write!(f, "{e}"),
RenderError::ChartParse(e) => write!(f, "{e}"),
RenderError::ChartHasNoExtent { what } => write!(f, "{what}"),
RenderError::NoFonts => write!(f, "no sans-serif font available for diagram labels"),
RenderError::NothingToDraw => write!(f, "flowchart has nothing to draw"),
}
}
}
impl std::error::Error for RenderError {}
impl From<ParseError> for RenderError {
fn from(e: ParseError) -> RenderError {
RenderError::Parse(e)
}
}
impl From<crate::preview::mermaid::state::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::state::ParseError) -> RenderError {
RenderError::StateParse(e)
}
}
impl From<crate::preview::mermaid::class::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::class::ParseError) -> RenderError {
RenderError::ClassParse(e)
}
}
impl From<crate::preview::mermaid::er::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::er::ParseError) -> RenderError {
RenderError::ErParse(e)
}
}
impl From<crate::preview::mermaid::sequence::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::sequence::ParseError) -> RenderError {
RenderError::SequenceParse(e)
}
}
impl From<crate::preview::mermaid::chart::ParseError> for RenderError {
fn from(e: crate::preview::mermaid::chart::ParseError) -> RenderError {
RenderError::ChartParse(e)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedNode {
pub id: String,
pub shape: Glyph,
pub center: Point,
pub size: Size,
pub label: Label,
pub panel: Option<Panel>,
pub series: Option<usize>,
pub mark: Option<shapes::Mark>,
pub style: Option<ShapeStyle>,
}
impl PlacedNode {
pub fn bounds(&self) -> (f64, f64, f64, f64) {
(
self.center.x - self.size.w / 2.0,
self.center.y - self.size.h / 2.0,
self.center.x + self.size.w / 2.0,
self.center.y + self.size.h / 2.0,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedEdgeLabel {
pub center: Point,
pub size: Size,
pub label: Label,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedEdge {
pub from: String,
pub to: String,
pub points: Vec<Point>,
pub gaps: Vec<(Point, Point)>,
pub tip_start: Tip,
pub tip_end: Tip,
pub stroke: Stroke,
pub label: Option<PlacedEdgeLabel>,
pub start_label: Option<PlacedEdgeLabel>,
pub end_label: Option<PlacedEdgeLabel>,
pub badge: Option<PlacedEdgeLabel>,
pub series: Option<usize>,
pub straight: bool,
pub overlay: bool,
pub style: Option<ShapeStyle>,
pub curve: Curve,
pub tip_matches_line: bool,
}
impl PlacedEdge {
pub fn drawn_points(&self) -> Vec<Point> {
if self.series.is_some() || self.straight || !self.curve.rounds_corners() {
return self.points.clone();
}
edges::fix_corners(&self.points)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedCluster {
pub id: String,
pub title: Label,
pub center: Point,
pub size: Size,
pub parent: Option<String>,
pub depth: usize,
pub dashed: bool,
pub filled: bool,
pub sections: Vec<ClusterSection>,
pub title_strip: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClusterSection {
pub y: f64,
pub title: Label,
}
impl PlacedCluster {
pub fn bounds(&self) -> (f64, f64, f64, f64) {
(
self.center.x - self.size.w / 2.0,
self.center.y - self.size.h / 2.0,
self.center.x + self.size.w / 2.0,
self.center.y + self.size.h / 2.0,
)
}
pub fn title_center(&self) -> Point {
let (_, top, _, _) = self.bounds();
Point::new(
self.center.x,
top + clusters::TITLE_PAD_Y + self.title.height / 2.0,
)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Diagram {
pub width: f64,
pub height: f64,
pub nodes: Vec<PlacedNode>,
pub edges: Vec<PlacedEdge>,
pub clusters: Vec<PlacedCluster>,
pub lifelines: Vec<PlacedLifeline>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedLifeline {
pub id: String,
pub x: f64,
pub top: f64,
pub bottom: f64,
pub destroyed: bool,
pub activations: Vec<PlacedActivation>,
}
impl PlacedLifeline {
pub fn activation_bounds(&self, i: usize) -> Option<(f64, f64, f64, f64)> {
let a = self.activations.get(i)?;
let left = self.x - ACTIVATION_WIDTH / 2.0 + a.depth as f64 * ACTIVATION_WIDTH;
Some((left, a.top, left + ACTIVATION_WIDTH, a.bottom))
}
pub fn extent(&self) -> (f64, f64) {
let mut left = self.x;
let mut right = self.x;
for i in 0..self.activations.len() {
if let Some((l, _, r, _)) = self.activation_bounds(i) {
left = left.min(l);
right = right.max(r);
}
}
(left, right)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlacedActivation {
pub depth: usize,
pub top: f64,
pub bottom: f64,
}
pub const ACTIVATION_WIDTH: f64 = 10.0;
impl Diagram {
pub fn node(&self, id: &str) -> Option<&PlacedNode> {
self.nodes.iter().find(|n| n.id == id)
}
pub fn cluster(&self, id: &str) -> Option<&PlacedCluster> {
self.clusters.iter().find(|c| c.id == id)
}
}
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
render_curve(code, theme, "basis")
}
pub fn render_curve(code: &str, theme: &str, curve: &str) -> Result<String, RenderError> {
render_flow(code, theme, curve, "splines")
}
pub fn render_flow(
code: &str,
theme: &str,
curve: &str,
routing: &str,
) -> Result<String, RenderError> {
let chart = flowchart::parse(code)?;
let diagram = lay_out_flow(&chart, curve, routing)?;
Ok(svg::emit(
&diagram,
&Theme::for_routing(theme, Routing::parse(routing)),
))
}
pub fn lay_out(chart: &Flowchart) -> Result<Diagram, RenderError> {
lay_out_curve(chart, "basis")
}
pub fn lay_out_curve(chart: &Flowchart, curve: &str) -> Result<Diagram, RenderError> {
lay_out_flow(chart, curve, "splines")
}
pub fn lay_out_flow(chart: &Flowchart, curve: &str, routing: &str) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if chart.nodes.is_empty() {
return Err(RenderError::NothingToDraw);
}
lay_out_spec(&spec_of(chart, curve, Routing::parse(routing)))
}
fn spec_of(chart: &Flowchart, curve: &str, routing: Routing) -> GraphSpec {
let curve = chart.curve.as_deref().unwrap_or(curve);
let class_of = |name: &str| {
chart
.class_defs
.iter()
.find(|d| d.name == name)
.map(|d| d.styles.as_slice())
};
let nodes = chart
.nodes
.iter()
.map(|node| {
let glyph = if routing == Routing::Orthogonal && node.shape == Shape::Diamond {
Glyph::ChamferedRect
} else {
Glyph::Flow(node.shape)
};
let (label, size) = (routing == Routing::Orthogonal)
.then(|| shapes::orthogonal_node(glyph, &node.label))
.flatten()
.unwrap_or_else(|| {
let label = Label::measure(&node.label);
let size = shapes::size(glyph, Size::new(label.width, label.height));
(label, size)
});
SpecNode {
id: node.id.clone(),
glyph,
label,
size,
panel: None,
style: style::cascade(class_of, &node.classes, &node.styles),
has_class: !node.classes.is_empty(),
}
})
.collect();
let blocks = chart
.subgraphs
.iter()
.map(|s| SpecBlock {
id: s.id.clone(),
title: s.title.clone(),
members: s.members.clone(),
dashed: false,
})
.collect();
let edges = chart
.edges
.iter()
.enumerate()
.map(|(i, edge)| {
let link_default = chart.link_styles.iter().filter_map(|ls| {
matches!(ls.target, LinkStyleTarget::Default).then_some(ls.styles.as_slice())
});
let link_indexed = chart.link_styles.iter().filter_map(|ls| match &ls.target {
LinkStyleTarget::Indices(idx) if idx.contains(&i) => Some(ls.styles.as_slice()),
_ => None,
});
let default_interpolate = chart
.link_styles
.iter()
.filter(|ls| matches!(ls.target, LinkStyleTarget::Default))
.filter_map(|ls| ls.interpolate.as_deref())
.next_back();
let indexed_interpolate = chart
.link_styles
.iter()
.filter(
|ls| matches!(&ls.target, LinkStyleTarget::Indices(idx) if idx.contains(&i)),
)
.filter_map(|ls| ls.interpolate.as_deref())
.next_back();
let resolved_curve = indexed_interpolate.or(default_interpolate).unwrap_or(curve);
SpecEdge {
id: edge.id.clone(),
from: edge.from.clone(),
to: edge.to.clone(),
label: edge
.label
.as_deref()
.map(Label::measure)
.filter(|l| !l.is_blank()),
tip_start: edges::Tip::of_arrow(edge.arrow).0,
tip_end: edges::Tip::of_arrow(edge.arrow).1,
stroke: edge.stroke,
minlen: edge.length,
start_label: None,
end_label: None,
style: style::cascade_edge(class_of, &edge.classes, link_default, link_indexed),
curve: Curve::parse(resolved_curve),
}
})
.collect();
GraphSpec {
direction: chart.direction,
nodes,
edges,
blocks,
routing,
fixed_self_loops: false,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpecNode {
pub id: String,
pub glyph: Glyph,
pub label: Label,
pub size: Size,
pub panel: Option<Panel>,
pub style: Option<ShapeStyle>,
pub has_class: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpecEdge {
pub id: String,
pub from: String,
pub to: String,
pub label: Option<Label>,
pub tip_start: Tip,
pub tip_end: Tip,
pub stroke: Stroke,
pub minlen: usize,
pub start_label: Option<Label>,
pub end_label: Option<Label>,
pub style: Option<ShapeStyle>,
pub curve: Curve,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpecBlock {
pub id: String,
pub title: String,
pub members: Vec<String>,
pub dashed: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GraphSpec {
pub direction: Direction,
pub nodes: Vec<SpecNode>,
pub edges: Vec<SpecEdge>,
pub blocks: Vec<SpecBlock>,
pub routing: Routing,
pub fixed_self_loops: bool,
}
pub fn lay_out_spec(spec: &GraphSpec) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if spec.nodes.is_empty() {
return Err(RenderError::NothingToDraw);
}
const MAX_GROWTH_PASSES: usize = 3;
let mut sizes: HashMap<String, Size> = HashMap::new();
let mut label_boosts: HashMap<String, f64> = HashMap::new();
let mut diagram: Option<Diagram> = None;
for _ in 0..MAX_GROWTH_PASSES {
let (this_diagram, required, label_shortfall) =
lay_out_spec_pass(spec, &sizes, &label_boosts)?;
let grew_nodes = apply_growth(spec, &mut sizes, &required);
let grew_labels = apply_label_growth(&mut label_boosts, &label_shortfall);
diagram = Some(this_diagram);
if !grew_nodes && !grew_labels {
break;
}
}
Ok(diagram.expect("the loop body runs at least once: MAX_GROWTH_PASSES > 0"))
}
fn apply_growth(
spec: &GraphSpec,
sizes: &mut HashMap<String, Size>,
required: &HashMap<String, Size>,
) -> bool {
let mut grew = false;
for node in &spec.nodes {
let Some(need) = required.get(&node.id) else {
continue;
};
let cur = sizes.get(&node.id).copied().unwrap_or(node.size);
let grown = Size::new(cur.w.max(need.w), cur.h.max(need.h));
if grown.w > cur.w + 1e-6 || grown.h > cur.h + 1e-6 {
sizes.insert(node.id.clone(), grown);
grew = true;
}
}
grew
}
fn bar_required_sizes(
direction: Direction,
nodes: &[PlacedNode],
by_id: &HashMap<String, usize>,
drawable: &[Drawable],
) -> HashMap<String, Size> {
let cross_coord = |p: &Point| match direction {
Direction::TopToBottom | Direction::BottomToTop => p.x,
Direction::LeftToRight | Direction::RightToLeft => p.y,
};
let is_bar = |id: &str| {
by_id
.get(id)
.is_some_and(|&i| matches!(nodes[i].shape, Glyph::Bar { .. }))
};
let mut spans: HashMap<String, (f64, f64)> = HashMap::new();
for d in drawable {
if d.tail == d.head {
continue;
}
if is_bar(&d.tail) {
if let Some(&hi) = by_id.get(&d.head) {
let c = cross_coord(&nodes[hi].center);
let e = spans
.entry(d.tail.clone())
.or_insert((f64::INFINITY, f64::NEG_INFINITY));
e.0 = e.0.min(c);
e.1 = e.1.max(c);
}
}
if is_bar(&d.head) {
if let Some(&ti) = by_id.get(&d.tail) {
let c = cross_coord(&nodes[ti].center);
let e = spans
.entry(d.head.clone())
.or_insert((f64::INFINITY, f64::NEG_INFINITY));
e.0 = e.0.min(c);
e.1 = e.1.max(c);
}
}
}
let mut out = HashMap::new();
for (id, (min_c, max_c)) in spans {
let Some(&i) = by_id.get(&id) else { continue };
let length = (max_c - min_c).max(0.0) + 2.0 * orthogonal::BAR_PORT_PAD;
let horizontal = matches!(nodes[i].shape, Glyph::Bar { horizontal: true });
let thickness = if horizontal {
nodes[i].size.h
} else {
nodes[i].size.w
};
let size = if horizontal {
Size::new(length, thickness)
} else {
Size::new(thickness, length)
};
out.insert(id, size);
}
out
}
fn apply_label_growth(boosts: &mut HashMap<String, f64>, shortfall: &HashMap<String, f64>) -> bool {
let mut grew = false;
for (id, extra) in shortfall {
if *extra > 1e-6 {
*boosts.entry(id.clone()).or_insert(0.0) += *extra;
grew = true;
}
}
grew
}
type LayoutPassResult = (Diagram, HashMap<String, Size>, HashMap<String, f64>);
fn is_aside(routing: Routing, stroke: Stroke) -> bool {
routing == Routing::Orthogonal && stroke == Stroke::Dotted
}
fn aside_weight(routing: Routing, stroke: Stroke) -> i32 {
if is_aside(routing, stroke) {
0
} else {
EdgeLabel::default().weight
}
}
fn lay_out_spec_pass(
spec: &GraphSpec,
sizes: &HashMap<String, Size>,
label_boosts: &HashMap<String, f64>,
) -> Result<LayoutPassResult, RenderError> {
let node_ids: HashSet<&str> = spec.nodes.iter().map(|n| n.id.as_str()).collect();
let tree = clusters::Tree::from_blocks(&spec.blocks, |id| node_ids.contains(id));
let mut g: Graph<NodeLabel, EdgeLabel> = Graph::with_options(GraphOptions {
directed: true,
multigraph: true,
compound: !tree.is_empty(),
});
let mut measured: HashMap<&str, &SpecNode> = HashMap::new();
for node in &spec.nodes {
let size = sizes.get(&node.id).copied().unwrap_or(node.size);
g.set_node(
node.id.clone(),
Some(NodeLabel {
width: size.w,
height: size.h,
..NodeLabel::default()
}),
);
measured.insert(node.id.as_str(), node);
}
for c in tree.iter() {
g.set_node(c.id.clone(), Some(NodeLabel::default()));
}
for c in tree.iter() {
if let Some(parent) = &c.parent {
g.set_parent(&c.id, Some(parent.as_str()));
}
for m in &c.member_nodes {
if g.has_node(m) {
g.set_parent(m, Some(c.id.as_str()));
}
}
}
let internal_edges: Vec<(String, String)> = spec
.edges
.iter()
.map(|e| (e.from.clone(), e.to.clone()))
.collect();
let (exit_role, entry_role) = if spec.routing == Routing::Orthogonal {
(clusters::AnchorRole::Exit, clusters::AnchorRole::Entry)
} else {
(
clusters::AnchorRole::Declared,
clusters::AnchorRole::Declared,
)
};
let mut drawable: Vec<Drawable> = Vec::new();
let mut edge_label_dims: HashMap<String, (f64, f64)> = HashMap::new();
for edge in &spec.edges {
let is_node = |id: &str| measured.contains_key(id);
let (Some(tail), Some(head)) = (
tree.anchor(&edge.from, &is_node, exit_role, &internal_edges),
tree.anchor(&edge.to, &is_node, entry_role, &internal_edges),
) else {
continue;
};
let (tail, head) = (tail.to_string(), head.to_string());
let (mut w, mut h) = edge
.label
.as_ref()
.map_or((0.0, 0.0), |l| (l.width, l.height));
if let Some(&boost) = label_boosts.get(&edge.id) {
if matches!(
spec.direction,
Direction::TopToBottom | Direction::BottomToTop
) {
h += boost;
} else {
w += boost;
}
}
g.set_edge(
tail.clone(),
head.clone(),
Some(EdgeLabel {
width: w,
height: h,
minlen: edge.minlen.max(1) as i32,
weight: aside_weight(spec.routing, edge.stroke),
rank_only: is_aside(spec.routing, edge.stroke),
..EdgeLabel::default()
}),
Some(edge.id.as_str()),
);
edge_label_dims.insert(edge.id.clone(), (w, h));
drawable.push(Drawable { edge, tail, head });
}
layout(
&mut g,
Some(LayoutOptions {
rankdir: rank_dir(spec.direction),
nodesep: if spec.routing == Routing::Orthogonal {
ORTHO_NODE_SEP
} else {
NODE_SEP
},
edgesep: EDGE_SEP,
ranksep: RANK_SEP,
marginx: MARGIN,
marginy: MARGIN,
tie_keep_first: true,
..LayoutOptions::default()
}),
);
if spec.routing == Routing::Orthogonal {
pull_back_fan_ranks(
&mut g,
spec.direction,
&drawable,
sizes,
&measured,
&edge_label_dims,
&tree,
);
}
let mut nodes: Vec<PlacedNode> = Vec::with_capacity(spec.nodes.len());
for node in &spec.nodes {
let placed = g.node(&node.id);
let center = Point::new(
placed.and_then(|n| n.x).unwrap_or(0.0),
placed.and_then(|n| n.y).unwrap_or(0.0),
);
let size = sizes.get(&node.id).copied().unwrap_or(node.size);
nodes.push(PlacedNode {
id: node.id.clone(),
shape: node.glyph,
center,
size,
label: node.label.clone(),
panel: node.panel.clone(),
series: None,
mark: None,
style: node.style.clone(),
});
}
if nodes.is_empty() {
return Err(RenderError::NothingToDraw);
}
let mut alignment_deltas: HashMap<String, f64> = HashMap::new();
let pre_pass_cross: Vec<f64> = nodes.iter().map(|n| cross_of(spec.direction, n)).collect();
let mut chain_next: HashMap<String, String> = HashMap::new();
let mut node_rank: HashMap<String, i32> = HashMap::new();
let lane_units = orthogonal::LaneUnits::build(&tree, &nodes);
if spec.routing == Routing::Orthogonal {
node_rank = nodes
.iter()
.filter_map(|n| {
g.node(&n.id)
.and_then(|nl| nl.rank)
.map(|r| (n.id.clone(), r))
})
.collect();
let candidates: Vec<(String, String)> = drawable
.iter()
.filter(|d| d.tail != d.head)
.map(|d| (d.tail.clone(), d.head.clone()))
.collect();
let (_probe_deltas, chain_next_probe) = orthogonal::align_straight_lanes(
spec.direction,
&mut nodes,
&node_rank,
&candidates,
&lane_units,
);
let has_class: HashMap<String, bool> = spec
.nodes
.iter()
.map(|n| (n.id.clone(), n.has_class))
.collect();
let touched = regroup_fan_lanes(
spec.direction,
&mut nodes,
&node_rank,
&candidates,
&chain_next_probe,
&has_class,
&lane_units,
);
if touched {
(_, chain_next) = orthogonal::align_straight_lanes_with(
spec.direction,
&mut nodes,
&node_rank,
&candidates,
Some(&chain_next_probe),
&lane_units,
);
} else {
chain_next = chain_next_probe;
}
let tier_edges: Vec<(String, String, bool)> = drawable
.iter()
.map(|d| {
(
d.tail.clone(),
d.head.clone(),
is_aside(spec.routing, d.edge.stroke),
)
})
.collect();
let tiers = orthogonal::place_dead_end_tiers(
spec.direction,
&mut nodes,
&tree,
&lane_units,
&chain_next,
&tier_edges,
);
for member in tiers.iter().flat_map(|t| t.members.iter()) {
if let Some(n) = g.node_mut(member) {
n.rank = None;
}
node_rank.remove(member);
chain_next.retain(|_, t| t != member);
}
alignment_deltas = nodes
.iter()
.enumerate()
.filter_map(|(i, n)| {
let delta = cross_of(spec.direction, n) - pre_pass_cross[i];
(delta.abs() > f64::EPSILON).then(|| (n.id.clone(), delta))
})
.collect();
}
let by_id: HashMap<String, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.clone(), i))
.collect();
let bar_min_sizes: HashMap<String, Size> = if spec.routing == Routing::Orthogonal {
bar_required_sizes(spec.direction, &nodes, &by_id, &drawable)
} else {
HashMap::new()
};
let mut placed_clusters = read_clusters(&g, &tree, &nodes, spec.routing);
if spec.routing == Routing::Orthogonal {
for _ in 0..2 {
if !orthogonal::clear_foreign_cluster_overlaps(
spec.direction,
&mut nodes,
&placed_clusters,
&tree,
) {
break;
}
rebuild_frames(&mut placed_clusters, &tree, &nodes);
}
}
let placed_clusters = placed_clusters;
let mut out_degree: HashMap<String, usize> = HashMap::new();
let mut in_degree: HashMap<String, usize> = HashMap::new();
for d in &drawable {
*out_degree.entry(d.edge.from.clone()).or_insert(0) += 1;
*in_degree.entry(d.edge.to.clone()).or_insert(0) += 1;
}
struct PreparedEdge<'a> {
edge: &'a SpecEdge,
tail_id: String,
head_id: String,
raw: Vec<Point>,
tail_end: edges::End,
head_end: edges::End,
}
let mut prepared: Vec<PreparedEdge> = Vec::with_capacity(drawable.len());
for Drawable {
edge,
tail: tail_id,
head: head_id,
} in &drawable
{
let edge = *edge;
let tail_id = tail_id.clone();
let head_id = head_id.clone();
let mut raw = g
.edge(&tail_id, &head_id, Some(edge.id.as_str()))
.map(|l| l.points.clone())
.unwrap_or_default();
if tail_id == head_id {
if let Some(&delta) = alignment_deltas.get(&tail_id) {
for p in &mut raw {
*p = orthogonal::shift_cross(spec.direction, p, delta);
}
}
}
let (Some(&ti), Some(&hi)) = (by_id.get(tail_id.as_str()), by_id.get(head_id.as_str()))
else {
continue;
};
let end_of = |written: &str, anchor: &PlacedNode| match placed_clusters
.iter()
.find(|c| c.id == written)
{
Some(c) => edges::End::Cluster(clusters::Rect::new(&c.center, c.size)),
None => edges::End::Node(anchor.shape, anchor.center.clone(), anchor.size),
};
let tail_end = end_of(&edge.from, &nodes[ti]);
let head_end = end_of(&edge.to, &nodes[hi]);
prepared.push(PreparedEdge {
edge,
tail_id,
head_id,
raw,
tail_end,
head_end,
});
}
let eligible: Vec<orthogonal::EligibleEdge> = if spec.routing == Routing::Orthogonal {
prepared
.iter()
.map(|p| {
let source = match p.tail_end {
edges::End::Cluster(_) => p.edge.from.as_str(),
edges::End::Node(..) => p.tail_id.as_str(),
};
let target = match p.head_end {
edges::End::Cluster(_) => p.edge.to.as_str(),
edges::End::Node(..) => p.head_id.as_str(),
};
orthogonal::EligibleEdge {
id: p.edge.id.as_str(),
source,
target,
raw: &p.raw,
source_rank: g.node(p.tail_id.as_str()).and_then(|n| n.rank),
target_rank: g.node(p.head_id.as_str()).and_then(|n| n.rank),
source_out_degree: out_degree.get(&p.edge.from).copied().unwrap_or(0),
target_in_degree: in_degree.get(&p.edge.to).copied().unwrap_or(0),
aside: is_aside(spec.routing, p.edge.stroke),
}
})
.collect()
} else {
Vec::new()
};
let (mut orthogonal_points, required_size): (
HashMap<String, Vec<Point>>,
HashMap<String, Size>,
) = if spec.routing == Routing::Orthogonal {
let mut routed = orthogonal::route_flowchart(
spec.direction,
&nodes,
&placed_clusters,
&eligible,
&chain_next,
spec.fixed_self_loops,
);
if tree.is_empty() {
let moved = reserve_pass_through_rows(
spec.direction,
&mut nodes,
&node_rank,
&drawable,
&routed.pass_through_eligible,
);
if moved {
routed = orthogonal::route_flowchart(
spec.direction,
&nodes,
&placed_clusters,
&eligible,
&chain_next,
spec.fixed_self_loops,
);
}
}
let orthogonal::RoutedFlowchart {
mut points,
mut required_size,
pass_through_eligible: _,
bar_geometry,
} = routed;
apply_bar_geometry(&mut nodes, &bar_geometry);
for (id, need) in bar_min_sizes {
let entry = required_size.entry(id).or_insert(Size::new(0.0, 0.0));
entry.w = entry.w.max(need.w);
entry.h = entry.h.max(need.h);
}
orthogonal::separate_coincident_detours(
spec.direction,
&nodes,
&placed_clusters,
&eligible,
&mut points,
);
(points, required_size)
} else {
(HashMap::new(), HashMap::new())
};
let mut orthogonal_index: HashMap<String, usize> = HashMap::new();
let mut label_shortfall: HashMap<String, f64> = HashMap::new();
let mut placed_edges: Vec<PlacedEdge> = Vec::with_capacity(prepared.len());
for PreparedEdge {
edge,
tail_id,
head_id: _,
raw,
tail_end,
head_end,
} in &prepared
{
let is_orthogonal = spec.routing == Routing::Orthogonal;
let (points, straight) = if is_orthogonal {
let pts = orthogonal_points
.get(edge.id.as_str())
.cloned()
.unwrap_or_else(|| edges::route(raw, tail_end, head_end));
(pts, true)
} else {
(edges::route(raw, tail_end, head_end), false)
};
let placed_label = edge.label.clone().and_then(|l| {
if is_orthogonal {
let size = Size::new(l.width + LABEL_PAD_X * 2.0, l.height + LABEL_PAD_Y * 2.0);
let slot = orthogonal::label_slot_clear(spec.direction, &points, &|center| {
orthogonal::plate_coverage(
center,
size,
edge.id.as_str(),
&orthogonal_points,
&nodes,
&placed_clusters,
)
})?;
if spec.fixed_self_loops && edge.from == edge.to {
if let Some(&ni) = by_id.get(tail_id.as_str()) {
let node = &nodes[ni];
let cross_gap = orthogonal::SELF_LOOP_LABEL_GAP
+ match spec.direction {
Direction::TopToBottom | Direction::BottomToTop => size.w / 2.0,
Direction::LeftToRight | Direction::RightToLeft => size.h / 2.0,
};
let center = match spec.direction {
Direction::TopToBottom | Direction::BottomToTop => {
let sign = if slot.center.x >= node.center.x {
1.0
} else {
-1.0
};
Point::new(slot.center.x + sign * cross_gap, slot.center.y)
}
Direction::LeftToRight | Direction::RightToLeft => {
let sign = if slot.center.y >= node.center.y {
1.0
} else {
-1.0
};
Point::new(slot.center.x, slot.center.y + sign * cross_gap)
}
};
return Some(PlacedEdgeLabel {
center,
size,
label: l,
});
}
}
if slot.is_flow_axis {
let need = orthogonal::label_min_length(size, slot.horizontal);
if slot.length + 1e-6 < need {
label_shortfall.insert(edge.id.clone(), need - slot.length);
}
}
Some(PlacedEdgeLabel {
center: slot.center,
size,
label: l,
})
} else {
edges::arc_midpoint(&points).map(|center| PlacedEdgeLabel {
center,
size: Size::new(l.width + LABEL_PAD_X * 2.0, l.height + LABEL_PAD_Y * 2.0),
label: l,
})
}
});
let side_label = |label: &Option<Label>, at_start: bool| -> Option<PlacedEdgeLabel> {
let l = label.clone()?;
let center = edges::end_label_anchor(&points, at_start, l.width, l.height)?;
Some(PlacedEdgeLabel {
center,
size: Size::new(l.width + LABEL_PAD_X * 2.0, l.height + LABEL_PAD_Y * 2.0),
label: l,
})
};
let start_label = side_label(&edge.start_label, true);
let end_label = side_label(&edge.end_label, false);
if is_orthogonal {
orthogonal_index.insert(edge.id.clone(), placed_edges.len());
}
let style = if is_orthogonal
&& edge
.style
.as_ref()
.and_then(|s| s.stroke.as_deref())
.is_none()
{
by_id
.get(edge.to.as_str())
.and_then(|&i| nodes[i].style.as_ref())
.and_then(|s| s.stroke.as_deref())
.and_then(style::lighten)
.map(|lightened| {
let mut s = edge.style.clone().unwrap_or_default();
s.stroke = Some(lightened.clone());
if s.text.is_none() {
s.text = Some(lightened);
}
s
})
.or_else(|| edge.style.clone())
} else {
edge.style.clone()
};
placed_edges.push(PlacedEdge {
from: edge.from.clone(),
to: edge.to.clone(),
points,
gaps: Vec::new(),
tip_start: edge.tip_start,
tip_end: edge.tip_end,
stroke: edge.stroke,
label: placed_label,
start_label,
end_label,
badge: None,
series: None,
straight,
overlay: false,
style,
curve: edge.curve,
tip_matches_line: straight,
});
}
if spec.routing == Routing::Orthogonal && !eligible.is_empty() {
let mut plates: HashMap<String, PlacedEdgeLabel> = HashMap::new();
for (id, &idx) in &orthogonal_index {
if let Some(l) = &placed_edges[idx].label {
plates.insert(id.clone(), l.clone());
}
}
orthogonal::avoid_label_plates(
spec.direction,
&nodes,
&placed_clusters,
&eligible,
&mut orthogonal_points,
&mut plates,
&chain_next,
);
for (id, &idx) in &orthogonal_index {
if let Some(new_points) = orthogonal_points.get(id) {
if *new_points != placed_edges[idx].points {
placed_edges[idx].points = new_points.clone();
if let Some(new_plate) = plates.get(id) {
placed_edges[idx].label = Some(new_plate.clone());
}
}
}
}
let gap_map = orthogonal::insert_crossing_gaps(
spec.direction,
&nodes,
&placed_clusters,
&eligible,
&orthogonal_points,
);
for (id, &idx) in &orthogonal_index {
if let Some(g) = gap_map.get(id) {
placed_edges[idx].gaps = g.clone();
}
}
}
let mut diagram = Diagram {
width: 0.0,
height: 0.0,
nodes,
edges: placed_edges,
clusters: placed_clusters,
lifelines: Vec::new(),
};
normalise(&mut diagram);
Ok((diagram, required_size, label_shortfall))
}
struct Drawable<'a> {
edge: &'a SpecEdge,
tail: String,
head: String,
}
fn pull_back_fan_ranks(
g: &mut Graph<NodeLabel, EdgeLabel>,
direction: Direction,
drawable: &[Drawable],
sizes: &HashMap<String, Size>,
measured: &HashMap<&str, &SpecNode>,
edge_label_dims: &HashMap<String, (f64, f64)>,
tree: &clusters::Tree,
) {
let old_rank = |g: &Graph<NodeLabel, EdgeLabel>, id: &str| -> i32 {
g.node(id).and_then(|n| n.rank).unwrap_or(0)
};
let mut ids: Vec<String> = measured.keys().map(|id| id.to_string()).collect();
ids.sort_by_key(|id| old_rank(g, id));
let unit_of: HashMap<&str, &str> = ids
.iter()
.map(|id| {
let unit = tree.outermost(id).unwrap_or(id.as_str());
(id.as_str(), unit)
})
.collect();
let mut unit_ranks: HashMap<&str, Vec<i32>> = HashMap::new();
for id in &ids {
unit_ranks
.entry(unit_of[id.as_str()])
.or_default()
.push(old_rank(g, id));
}
{
let mut leaves: std::collections::HashSet<(&str, &str)> = std::collections::HashSet::new();
let mut returns: std::collections::HashSet<(&str, &str)> = std::collections::HashSet::new();
for d in drawable {
if d.tail == d.head {
continue;
}
let (Some(&tail_unit), Some(&head_unit)) =
(unit_of.get(d.tail.as_str()), unit_of.get(d.head.as_str()))
else {
continue;
};
if tail_unit == head_unit {
continue;
}
leaves.insert((tail_unit, d.head.as_str()));
returns.insert((head_unit, d.tail.as_str()));
}
for (unit, outside) in leaves.intersection(&returns) {
let r = old_rank(g, outside);
let Some(rs) = unit_ranks.get_mut(*unit) else {
continue;
};
let (Some(&lo), Some(&hi)) = (rs.iter().min(), rs.iter().max()) else {
continue;
};
if r > lo && r < hi && !rs.contains(&r) {
rs.push(r);
}
}
}
for rs in unit_ranks.values_mut() {
rs.sort_unstable();
rs.dedup();
}
let level_of = |id: &str| -> i32 {
let r = old_rank(g, id);
unit_ranks
.get(unit_of[id])
.and_then(|rs| rs.iter().position(|&x| x == r))
.unwrap_or(0) as i32
};
let mut incoming: HashMap<&str, Vec<(&str, i32)>> = HashMap::new();
for d in drawable {
if d.tail == d.head {
continue;
}
let (sr, tr) = (old_rank(g, &d.tail), old_rank(g, &d.head));
if tr <= sr {
continue;
}
let (Some(&tail_unit), Some(&head_unit)) =
(unit_of.get(d.tail.as_str()), unit_of.get(d.head.as_str()))
else {
continue;
};
if tail_unit == head_unit {
continue;
}
let block_level = |written: &str, exit: bool| -> Option<i32> {
if !tree.contains(written) {
return None;
}
let levels = tree
.descendants(written)
.into_iter()
.filter(|m| unit_of.contains_key(m))
.map(level_of);
if exit {
levels.max()
} else {
levels.min()
}
};
let minlen = d.edge.minlen.max(1) as i32;
let tail_level = block_level(&d.edge.from, true).unwrap_or_else(|| level_of(&d.tail));
let head_level = block_level(&d.edge.to, false).unwrap_or_else(|| level_of(&d.head));
let offset = tail_level + minlen - head_level;
incoming
.entry(head_unit)
.or_default()
.push((tail_unit, offset));
}
let mut units: Vec<&str> = unit_ranks.keys().copied().collect();
units.sort_unstable();
let mut start: HashMap<&str, i32> = units.iter().map(|u| (*u, 0)).collect();
let mut settled = false;
for _ in 0..=units.len() {
let mut changed = false;
for u in &units {
let Some(preds) = incoming.get(*u) else {
continue;
};
let want = preds
.iter()
.map(|(p, offset)| start.get(*p).copied().unwrap_or(0) + offset)
.max()
.unwrap_or(0)
.max(0);
if want > start[*u] {
start.insert(u, want);
changed = true;
}
}
if !changed {
settled = true;
break;
}
}
if !settled {
return;
}
let new_rank: HashMap<String, i32> = ids
.iter()
.map(|id| {
let r = start[unit_of[id.as_str()]] + level_of(id);
(id.clone(), r)
})
.collect();
let flow_extent = |id: &str| -> f64 {
let size = sizes
.get(id)
.copied()
.or_else(|| measured.get(id).map(|n| n.size))
.unwrap_or(Size::new(0.0, 0.0));
match direction {
Direction::TopToBottom | Direction::BottomToTop => size.h / 2.0,
Direction::LeftToRight | Direction::RightToLeft => size.w / 2.0,
}
};
let mut distinct_ranks: Vec<i32> = new_rank.values().copied().collect();
distinct_ranks.sort_unstable();
distinct_ranks.dedup();
let rank_index: HashMap<i32, usize> = distinct_ranks
.iter()
.enumerate()
.map(|(i, &r)| (r, i))
.collect();
let mut extra_gap: HashMap<usize, f64> = HashMap::new();
for d in drawable {
if d.tail == d.head {
continue;
}
let (Some(&tr), Some(&hr)) = (new_rank.get(d.tail.as_str()), new_rank.get(d.head.as_str()))
else {
continue;
};
if hr <= tr {
continue;
}
let Some(&i) = rank_index.get(&tr) else {
continue;
};
let label_dim = edge_label_dims
.get(&d.edge.id)
.map(|&(w, h)| match direction {
Direction::TopToBottom | Direction::BottomToTop => h,
Direction::LeftToRight | Direction::RightToLeft => w,
})
.unwrap_or(0.0);
if label_dim > 0.0 {
let entry = extra_gap.entry(i).or_insert(0.0);
if label_dim > *entry {
*entry = label_dim;
}
}
}
let head_of = |block: &clusters::Cluster| -> f64 {
let title = Label::measure(&block.title);
clusters::PAD
+ if title.is_blank() {
0.0
} else {
title.height + clusters::TITLE_PAD_Y * 2.0
}
};
let span_of: HashMap<&str, (i32, i32)> = tree
.iter()
.filter_map(|block| {
let member_ranks: Vec<i32> = tree
.descendants(&block.id)
.iter()
.filter_map(|m| new_rank.get(*m).copied())
.collect();
let (&first, &last) = (member_ranks.iter().min()?, member_ranks.iter().max()?);
Some((block.id.as_str(), (first, last)))
})
.collect();
let (mut frame_head, mut frame_tail): (HashMap<i32, f64>, HashMap<i32, f64>) =
(HashMap::new(), HashMap::new());
for block in tree.iter() {
let Some(&(first, last)) = span_of.get(block.id.as_str()) else {
continue;
};
let mut chain_head = 0.0;
let mut chain_tail = 0.0;
let mut cur = Some(block);
for _ in 0..=tree.iter().len() {
let Some(b) = cur else { break };
match span_of.get(b.id.as_str()) {
Some(&(f, _)) if f == first => chain_head += head_of(b),
_ => {}
}
match span_of.get(b.id.as_str()) {
Some(&(_, l)) if l == last => chain_tail += clusters::PAD,
_ => {}
}
cur = b.parent.as_deref().and_then(|p| tree.get(p));
}
let head_entry = frame_head.entry(first).or_insert(0.0);
*head_entry = head_entry.max(chain_head);
let tail_entry = frame_tail.entry(last).or_insert(0.0);
*tail_entry = tail_entry.max(chain_tail);
}
let mut column_flow: HashMap<i32, f64> = HashMap::new();
let mut cursor = MARGIN;
let mut prev_half = 0.0_f64;
let mut prev_tail = 0.0_f64;
for (i, &r) in distinct_ranks.iter().enumerate() {
let half = ids
.iter()
.filter(|id| new_rank.get(id.as_str()) == Some(&r))
.map(|id| flow_extent(id))
.fold(0.0_f64, f64::max);
let head = frame_head.get(&r).copied().unwrap_or(0.0);
let pos = if i == 0 {
MARGIN + head + half
} else {
let extra = i
.checked_sub(1)
.and_then(|p| extra_gap.get(&p))
.copied()
.unwrap_or(0.0);
cursor + prev_half + prev_tail + RANK_SEP + extra + head + half
};
column_flow.insert(r, pos);
cursor = pos;
prev_half = half;
prev_tail = frame_tail.get(&r).copied().unwrap_or(0.0);
}
for id in &ids {
let Some(&r) = new_rank.get(id.as_str()) else {
continue;
};
let Some(&flow_pos) = column_flow.get(&r) else {
continue;
};
if let Some(node) = g.node_mut(id) {
node.rank = Some(r);
match direction {
Direction::TopToBottom | Direction::BottomToTop => node.y = Some(flow_pos),
Direction::LeftToRight | Direction::RightToLeft => node.x = Some(flow_pos),
}
}
}
}
fn cross_of(direction: Direction, n: &PlacedNode) -> f64 {
match direction {
Direction::TopToBottom | Direction::BottomToTop => n.center.x,
Direction::LeftToRight | Direction::RightToLeft => n.center.y,
}
}
fn flow_of(direction: Direction, n: &PlacedNode) -> f64 {
match direction {
Direction::TopToBottom | Direction::BottomToTop => n.center.y,
Direction::LeftToRight | Direction::RightToLeft => n.center.x,
}
}
fn cross_extent_of(direction: Direction, n: &PlacedNode) -> f64 {
match direction {
Direction::TopToBottom | Direction::BottomToTop => n.size.w / 2.0,
Direction::LeftToRight | Direction::RightToLeft => n.size.h / 2.0,
}
}
enum Slot {
Frame(String),
Member { id: String, lead: f64, trail: f64 },
}
fn fan_split(
member_ids: &[String],
rest: &mut [String],
continues: &impl Fn(&str) -> bool,
) -> usize {
if member_ids.len() > orthogonal::FAN_ELIGIBLE_MIN_BRANCHES {
return member_ids.len() / 2;
}
rest.sort_by_key(|m| !continues(m));
match rest {
[only] => usize::from(continues(only)),
_ => member_ids.len() / 2,
}
}
fn regroup_fan_lanes(
direction: Direction,
nodes: &mut [PlacedNode],
node_rank: &HashMap<String, i32>,
candidates: &[(String, String)],
chain_next: &HashMap<String, String>,
has_class: &HashMap<String, bool>,
lane_units: &orthogonal::LaneUnits,
) -> bool {
let mut touched = false;
let id_index: HashMap<String, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.clone(), i))
.collect();
let mut by_rank: HashMap<i32, Vec<usize>> = HashMap::new();
for (id, &r) in node_rank {
if let Some(&i) = id_index.get(id) {
by_rank.entry(r).or_default().push(i);
}
}
let decl_index: HashMap<String, usize> = candidates
.iter()
.enumerate()
.map(|(i, (_, t))| (t.clone(), i))
.collect();
let decl_of = |id: &str| decl_index.get(id).copied().unwrap_or(usize::MAX);
let mut ranks: Vec<i32> = by_rank.keys().copied().collect();
ranks.sort_unstable();
for r in ranks {
let idxs = &by_rank[&r];
if idxs.len() < 2 {
continue; }
let member_ids: Vec<String> = idxs.iter().map(|&i| nodes[i].id.clone()).collect();
let mut feeders: Vec<&str> = candidates
.iter()
.filter(|(_, t)| member_ids.contains(t))
.map(|(s, _)| s.as_str())
.filter(|s| {
member_ids
.iter()
.all(|m| candidates.iter().any(|(cs, ct)| cs == s && ct == m))
})
.collect();
feeders.sort_unstable();
feeders.dedup();
let [source] = feeders.as_slice() else {
continue;
};
let source = (*source).to_string();
let trunk_id: Option<String> = chain_next
.get(&source)
.filter(|t| member_ids.contains(t))
.cloned();
let mut buckets: Vec<(String, Vec<String>)> = Vec::new();
let mut none_bucket: Vec<String> = Vec::new();
for id in &member_ids {
if trunk_id.as_ref() == Some(id) {
continue;
}
let classed = has_class.get(id).copied().unwrap_or(true);
if !classed {
none_bucket.push(id.clone());
continue;
}
let key = nodes[id_index[id]]
.style
.as_ref()
.and_then(|s| s.stroke.clone())
.unwrap_or_else(|| format!("\0{id}"));
match buckets.iter_mut().find(|(k, _)| *k == key) {
Some((_, members)) => members.push(id.clone()),
None => buckets.push((key, vec![id.clone()])),
}
}
buckets.sort_by_key(|(_, members)| {
members
.iter()
.map(|m| decl_of(m))
.min()
.unwrap_or(usize::MAX)
});
let nearest_forward_rank = |m: &str| -> i32 {
candidates
.iter()
.filter(|(s, _)| s == m)
.filter_map(|(_, t)| node_rank.get(t))
.min()
.copied()
.unwrap_or(i32::MAX)
};
for (_, members) in &mut buckets {
members.sort_by_key(|m| (nearest_forward_rank(m), decl_of(m)));
}
none_bucket.sort_by_key(|m| decl_of(m));
let mut rest: Vec<String> = buckets.into_iter().flat_map(|(_, m)| m).collect();
rest.extend(none_bucket);
let continues = |id: &str| candidates.iter().any(|(s, _)| s == id);
let new_order: Vec<String> = match &trunk_id {
Some(t) => {
let before = fan_split(&member_ids, &mut rest, &continues);
let split = before.min(rest.len());
let mut order: Vec<String> = rest[..split].to_vec();
order.push(t.clone());
order.extend_from_slice(&rest[split..]);
order
}
None => rest,
};
let mid = id_index
.get(&source)
.map(|&i| cross_of(direction, &nodes[i]))
.unwrap_or_else(|| {
let (mut top, mut bottom) = (f64::INFINITY, f64::NEG_INFINITY);
for &i in idxs {
let half = cross_extent_of(direction, &nodes[i]);
let c = cross_of(direction, &nodes[i]);
top = top.min(c - half);
bottom = bottom.max(c + half);
}
(top + bottom) / 2.0
});
let body_of = |id: &str| lane_units.separating_unit(id, &source).to_string();
let mut bodies: Vec<String> = Vec::new();
for id in &new_order {
let body = body_of(id);
if !bodies.contains(&body) {
bodies.push(body);
}
}
let new_order: Vec<String> = bodies
.iter()
.flat_map(|body| {
new_order
.iter()
.filter(|id| body_of(id) == *body)
.cloned()
.collect::<Vec<String>>()
})
.collect();
let mut current: Vec<String> = member_ids.clone();
current.sort_by(|a, b| {
cross_of(direction, &nodes[id_index[a]])
.partial_cmp(&cross_of(direction, &nodes[id_index[b]]))
.unwrap_or(std::cmp::Ordering::Equal)
});
let trunk_aligned = trunk_id
.as_ref()
.map(|t| (cross_of(direction, &nodes[id_index[t]]) - mid).abs() < 0.01)
.unwrap_or(true); if current == new_order && trunk_aligned {
continue;
}
let runs: Vec<(String, Vec<String>)> = bodies
.iter()
.map(|body| {
let members: Vec<String> = new_order
.iter()
.filter(|id| body_of(id) == *body)
.cloned()
.collect();
(body.clone(), members)
})
.collect();
let mut slots: Vec<Slot> = Vec::new();
for (body, members) in &runs {
match members.as_slice() {
[only] if lane_units.is_block(body) => slots.push(Slot::Frame(body.clone())),
_ => {
let pad = lane_units.frame_pad(body);
let last = members.len() - 1;
for (i, id) in members.iter().enumerate() {
slots.push(Slot::Member {
id: id.clone(),
lead: if i == 0 { pad } else { 0.0 },
trail: if i == last { pad } else { 0.0 },
});
}
}
}
}
let mut current: Vec<String> = member_ids.clone();
current.sort_by(|a, b| {
cross_of(direction, &nodes[id_index[a]])
.partial_cmp(&cross_of(direction, &nodes[id_index[b]]))
.unwrap_or(std::cmp::Ordering::Equal)
});
let trunk_aligned = trunk_id
.as_ref()
.map(|t| (cross_of(direction, &nodes[id_index[t]]) - mid).abs() < 0.01)
.unwrap_or(true); if current == new_order && trunk_aligned {
continue;
}
let extent = |nodes: &[PlacedNode], slot: &Slot| match slot {
Slot::Frame(body) => lane_units.band(direction, nodes, &id_index, body),
Slot::Member { id, lead, trail } => {
let n = &nodes[id_index[id]];
let (c, half) = (cross_of(direction, n), cross_extent_of(direction, n));
(c - half - lead, c + half + trail)
}
};
let slide = |nodes: &mut [PlacedNode], slot: &Slot, delta: f64| match slot {
Slot::Frame(body) => lane_units.shift(direction, nodes, &id_index, body, delta),
Slot::Member { id, .. } => {
let idx = id_index[id];
let flow_v = flow_of(direction, &nodes[idx]);
let new_c = cross_of(direction, &nodes[idx]) + delta;
nodes[idx].center = match direction {
Direction::TopToBottom | Direction::BottomToTop => Point::new(new_c, flow_v),
Direction::LeftToRight | Direction::RightToLeft => Point::new(flow_v, new_c),
};
}
};
touched = true;
match trunk_id.as_ref().and_then(|t| {
slots.iter().position(|slot| match slot {
Slot::Frame(body) => *body == body_of(t),
Slot::Member { id, .. } => id == t,
})
}) {
Some(tp) => {
let trunk = trunk_id
.as_ref()
.expect("the position came from `trunk_id`");
let delta = mid - cross_of(direction, &nodes[id_index[trunk]]);
slide(nodes, &slots[tp], delta);
let (lo, hi) = extent(nodes, &slots[tp]);
let mut edge = lo;
for slot in slots[..tp].iter().rev() {
let (slot_lo, slot_hi) = extent(nodes, slot);
let delta = (edge - ORTHO_NODE_SEP) - slot_hi;
slide(nodes, slot, delta);
edge = slot_lo + delta;
}
let mut edge = hi;
for slot in &slots[tp + 1..] {
let (slot_lo, slot_hi) = extent(nodes, slot);
let delta = (edge + ORTHO_NODE_SEP) - slot_lo;
slide(nodes, slot, delta);
edge = slot_hi + delta;
}
}
None => {
let total: f64 = slots
.iter()
.map(|slot| {
let (lo, hi) = extent(nodes, slot);
hi - lo
})
.sum::<f64>()
+ ORTHO_NODE_SEP * (slots.len().saturating_sub(1)) as f64;
let mut cursor = mid - total / 2.0;
for slot in &slots {
let (slot_lo, slot_hi) = extent(nodes, slot);
let delta = cursor - slot_lo;
slide(nodes, slot, delta);
cursor = slot_hi + delta + ORTHO_NODE_SEP;
}
}
}
}
touched
}
const PASS_THROUGH_CLEARANCE: f64 = 12.0;
#[must_use]
fn reserve_pass_through_rows(
direction: Direction,
nodes: &mut [PlacedNode],
node_rank: &HashMap<String, i32>,
drawable: &[Drawable],
eligible: &std::collections::HashSet<String>,
) -> bool {
let mut moved = false;
let id_index: HashMap<String, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.clone(), i))
.collect();
let mut connected: std::collections::HashSet<(String, String)> =
std::collections::HashSet::new();
for d in drawable {
if d.tail == d.head {
continue;
}
connected.insert((d.tail.clone(), d.head.clone()));
connected.insert((d.head.clone(), d.tail.clone()));
}
let is_connected = |a: &str, b: &str| connected.contains(&(a.to_string(), b.to_string()));
let mut by_rank: HashMap<i32, Vec<usize>> = HashMap::new();
for (id, &r) in node_rank {
if let Some(&i) = id_index.get(id) {
by_rank.entry(r).or_default().push(i);
}
}
for d in drawable {
if d.tail == d.head {
continue;
}
if !eligible.contains(&d.edge.id) {
continue; }
let (Some(&sr), Some(&tr)) = (node_rank.get(&d.tail), node_rank.get(&d.head)) else {
continue;
};
if tr - sr < 2 {
continue;
}
let Some(&source_idx) = id_index.get(&d.tail) else {
continue;
};
let row = cross_of(direction, &nodes[source_idx]);
for ri in (sr + 1)..tr {
let Some(members) = by_rank.get(&ri) else {
continue;
};
for &idx in members {
let id = nodes[idx].id.clone();
if id == d.tail || id == d.head {
continue;
}
if is_connected(&d.tail, &id) {
continue;
}
let half = cross_extent_of(direction, &nodes[idx]);
let delta = cross_of(direction, &nodes[idx]) - row;
if delta.abs() >= half + PASS_THROUGH_CLEARANCE {
continue; }
let sign = if delta.abs() < half {
-1.0
} else {
delta.signum()
};
let mut new_c = row + sign * (half + PASS_THROUGH_CLEARANCE);
for &other in members {
if other == idx {
continue;
}
let other_half = cross_extent_of(direction, &nodes[other]);
let other_c = cross_of(direction, &nodes[other]);
let min_gap = half + other_half + ORTHO_NODE_SEP;
if (new_c - other_c).abs() < min_gap {
new_c = other_c + sign * min_gap;
}
}
let flow_v = flow_of(direction, &nodes[idx]);
nodes[idx].center = match direction {
Direction::TopToBottom | Direction::BottomToTop => Point::new(new_c, flow_v),
Direction::LeftToRight | Direction::RightToLeft => Point::new(flow_v, new_c),
};
moved = true;
}
}
}
moved
}
fn read_clusters(
g: &Graph<NodeLabel, EdgeLabel>,
tree: &clusters::Tree,
nodes: &[PlacedNode],
routing: Routing,
) -> Vec<PlacedCluster> {
let mut out: Vec<PlacedCluster> = Vec::new();
for c in tree.iter() {
let Some(n) = g.node(&c.id) else { continue };
let (Some(x), Some(y)) = (n.x, n.y) else {
continue;
};
out.push(PlacedCluster {
id: c.id.clone(),
title: Label::measure(&c.title),
center: Point::new(x, y),
size: Size::new(n.width, n.height),
parent: c.parent.clone(),
depth: c.depth,
dashed: c.dashed,
filled: true,
sections: Vec::new(),
title_strip: false,
});
}
let placed: HashSet<String> = out.iter().map(|c| c.id.clone()).collect();
for c in &mut out {
if c.parent.as_ref().is_some_and(|p| !placed.contains(p)) {
c.parent = None;
}
}
out.sort_by_key(|c| c.depth);
match routing {
Routing::Splines => fit_titles(&mut out, tree, nodes),
Routing::Orthogonal => rebuild_frames(&mut out, tree, nodes),
}
out
}
fn apply_bar_geometry(nodes: &mut [PlacedNode], geometry: &HashMap<String, (Point, Size)>) {
for node in nodes {
if let Some((center, size)) = geometry.get(&node.id) {
node.center = center.clone();
node.size = *size;
}
}
}
fn rebuild_frames(clusters: &mut [PlacedCluster], tree: &clusters::Tree, nodes: &[PlacedNode]) {
let mut order: Vec<usize> = (0..clusters.len()).collect();
order.sort_by(|a, b| clusters[*b].depth.cmp(&clusters[*a].depth));
for i in order {
let id = clusters[i].id.clone();
let mut rect: Option<clusters::Rect> = None;
let hold = |r: clusters::Rect, rect: &mut Option<clusters::Rect>| match rect {
Some(cur) => cur.absorb(&r),
None => *rect = Some(r),
};
for c in clusters.iter() {
if c.parent.as_deref() == Some(id.as_str()) {
hold(clusters::Rect::new(&c.center, c.size), &mut rect);
}
}
if let Some(block) = tree.get(&id) {
for m in &block.member_nodes {
if let Some(n) = nodes.iter().find(|n| &n.id == m) {
let (l, t, r, b) = n.bounds();
hold(
clusters::Rect {
left: l,
top: t,
right: r,
bottom: b,
},
&mut rect,
);
}
}
}
let Some(mut rect) = rect else { continue };
rect.left -= clusters::PAD;
rect.right += clusters::PAD;
rect.top -= clusters::PAD;
rect.bottom += clusters::PAD;
let title = clusters[i].title.clone();
if !title.is_blank() {
rect.top -= title.height + clusters::TITLE_PAD_Y * 2.0;
let short = (title.width + clusters::TITLE_PAD_X) - rect.size().w;
if short > 0.0 {
rect.left -= short / 2.0;
rect.right += short / 2.0;
}
}
clusters[i].center = rect.center();
clusters[i].size = rect.size();
}
}
fn fit_titles(clusters: &mut [PlacedCluster], tree: &clusters::Tree, nodes: &[PlacedNode]) {
let mut order: Vec<usize> = (0..clusters.len()).collect();
order.sort_by(|a, b| clusters[*b].depth.cmp(&clusters[*a].depth));
for i in order {
let id = clusters[i].id.clone();
let mut rect = clusters::Rect::new(&clusters[i].center, clusters[i].size);
let mut top_child = f64::INFINITY;
for c in clusters.iter() {
if c.parent.as_deref() == Some(id.as_str()) {
let child = clusters::Rect::new(&c.center, c.size);
rect.absorb(&child);
top_child = top_child.min(child.top);
}
}
if let Some(block) = tree.get(&id) {
for m in &block.member_nodes {
if let Some(n) = nodes.iter().find(|n| &n.id == m) {
let (l, t, r, b) = n.bounds();
rect.absorb(&clusters::Rect {
left: l,
top: t,
right: r,
bottom: b,
});
top_child = top_child.min(t);
}
}
}
let title = clusters[i].title.clone();
if !title.is_blank() {
let need = title.width + clusters::TITLE_PAD_X;
let short = need - rect.size().w;
if short > 0.0 {
rect.left -= short / 2.0;
rect.right += short / 2.0;
}
let need = title.height + clusters::TITLE_PAD_Y * 2.0;
if top_child.is_finite() {
let short = need - (top_child - rect.top);
if short > 0.0 {
rect.top -= short;
}
}
}
clusters[i].center = rect.center();
clusters[i].size = rect.size();
}
}
pub const LABEL_PAD_X: f64 = 4.0;
pub const LABEL_PAD_Y: f64 = 1.0;
fn normalise(diagram: &mut Diagram) {
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut grow = |x0: f64, y0: f64, x1: f64, y1: f64| {
min_x = min_x.min(x0);
min_y = min_y.min(y0);
max_x = max_x.max(x1);
max_y = max_y.max(y1);
};
for n in &diagram.nodes {
let (l, t, r, b) = n.bounds();
grow(l, t, r, b);
}
for c in &diagram.clusters {
let (l, t, r, b) = c.bounds();
grow(l, t, r, b);
}
for l in &diagram.lifelines {
let (left, right) = l.extent();
grow(left, l.top, right, l.bottom);
}
for e in &diagram.edges {
for p in e.drawn_points() {
grow(p.x, p.y, p.x, p.y);
}
for l in [&e.label, &e.start_label, &e.end_label, &e.badge]
.into_iter()
.flatten()
{
grow(
l.center.x - l.size.w / 2.0,
l.center.y - l.size.h / 2.0,
l.center.x + l.size.w / 2.0,
l.center.y + l.size.h / 2.0,
);
}
}
if !min_x.is_finite() {
return;
}
let (dx, dy) = (MARGIN - min_x, MARGIN - min_y);
for n in &mut diagram.nodes {
n.center = Point::new(n.center.x + dx, n.center.y + dy);
}
for c in &mut diagram.clusters {
c.center = Point::new(c.center.x + dx, c.center.y + dy);
for section in &mut c.sections {
section.y += dy;
}
}
for l in &mut diagram.lifelines {
l.x += dx;
l.top += dy;
l.bottom += dy;
for a in &mut l.activations {
a.top += dy;
a.bottom += dy;
}
}
for e in &mut diagram.edges {
for p in &mut e.points {
*p = Point::new(p.x + dx, p.y + dy);
}
for (a, b) in &mut e.gaps {
*a = Point::new(a.x + dx, a.y + dy);
*b = Point::new(b.x + dx, b.y + dy);
}
for l in [
&mut e.label,
&mut e.start_label,
&mut e.end_label,
&mut e.badge,
]
.into_iter()
.flatten()
{
l.center = Point::new(l.center.x + dx, l.center.y + dy);
}
}
diagram.width = (max_x - min_x) + MARGIN * 2.0;
diagram.height = (max_y - min_y) + MARGIN * 2.0;
}
fn rank_dir(direction: Direction) -> RankDir {
match direction {
Direction::TopToBottom => RankDir::TB,
Direction::BottomToTop => RankDir::BT,
Direction::LeftToRight => RankDir::LR,
Direction::RightToLeft => RankDir::RL,
}
}