#![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;
#[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>,
}
#[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::named(theme)))
}
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 label = Label::measure(&node.label);
let glyph = if routing == Routing::Orthogonal && node.shape == Shape::Diamond {
Glyph::ChamferedRect
} else {
Glyph::Flow(node.shape)
};
let size = shapes::size(glyph, Size::new(label.width, label.height));
SpecNode {
id: node.id.clone(),
glyph,
label,
size,
panel: None,
style: style::cascade(class_of, &node.classes, &node.styles),
}
})
.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,
}
}
#[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>,
}
#[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 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 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 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 mut drawable: Vec<Drawable> = Vec::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),
tree.anchor(&edge.to, &is_node),
) 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,
..EdgeLabel::default()
}),
Some(edge.id.as_str()),
);
drawable.push(Drawable { edge, tail, head });
}
layout(
&mut g,
Some(LayoutOptions {
rankdir: rank_dir(spec.direction),
nodesep: NODE_SEP,
edgesep: EDGE_SEP,
ranksep: RANK_SEP,
marginx: MARGIN,
marginy: MARGIN,
tie_keep_first: true,
..LayoutOptions::default()
}),
);
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();
if spec.routing == Routing::Orthogonal {
let node_rank: HashMap<String, i32> = 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.edge.from == d.tail && d.edge.to == d.head)
.map(|d| (d.tail.clone(), d.head.clone()))
.collect();
alignment_deltas =
orthogonal::align_straight_lanes(spec.direction, &mut nodes, &node_rank, &candidates);
}
let by_id: HashMap<&str, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.as_str(), i))
.collect();
let placed_clusters = read_clusters(&g, &tree, &nodes);
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 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),
}
})
.collect()
} else {
Vec::new()
};
let (mut orthogonal_points, required_size): (
HashMap<String, Vec<Point>>,
HashMap<String, Size>,
) = if spec.routing == Routing::Orthogonal {
let orthogonal::RoutedFlowchart {
mut points,
required_size,
} = orthogonal::route_flowchart(spec.direction, &nodes, &placed_clusters, &eligible);
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 slot = orthogonal::label_slot(spec.direction, &points)?;
let size = Size::new(l.width + LABEL_PAD_X * 2.0, l.height + LABEL_PAD_Y * 2.0);
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());
}
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: edge.style.clone(),
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,
);
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 read_clusters(
g: &Graph<NodeLabel, EdgeLabel>,
tree: &clusters::Tree,
nodes: &[PlacedNode],
) -> 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(),
});
}
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);
fit_titles(&mut out, tree, nodes);
out
}
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,
}
}