use std::collections::HashMap;
use crate::preview::mermaid::block::{BlockDiagram, Composite, Dir, Item, Node, AUTO};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics;
use super::band;
use super::shapes::Mark;
use super::{
edges, normalise, shapes, svg, Diagram, Glyph, Label, PlacedCluster, PlacedEdge,
PlacedEdgeLabel, PlacedNode, RenderError, Size, Theme,
};
pub const CELL_GAP: f64 = 12.0;
pub const FRAME_PAD: f64 = 10.0;
pub const MIN_CELL: f64 = 56.0;
pub const DETOUR: f64 = 22.0;
pub const MIN_ROW: f64 = 28.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let diagram = crate::preview::mermaid::block::parse(code)?;
let laid = lay_out(&diagram)?;
Ok(svg::emit(&laid, &Theme::named(theme)))
}
pub fn lay_out(diagram: &BlockDiagram) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if diagram.items.is_empty() {
return Err(RenderError::NothingToDraw);
}
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut clusters: Vec<PlacedCluster> = Vec::new();
let grid = place(
&diagram.items,
diagram.columns,
0,
0.0,
0.0,
&mut nodes,
&mut clusters,
);
let _ = grid;
let by_id: HashMap<&str, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.as_str(), i))
.collect();
let mut placed: Vec<PlacedEdge> = Vec::new();
for edge in &diagram.edges {
let ends = |id: &str| -> Option<(Point, Size, Glyph)> {
if let Some(&i) = by_id.get(id) {
return Some((nodes[i].center.clone(), nodes[i].size, nodes[i].shape));
}
clusters.iter().find(|c| c.id == id).map(|c| {
(
c.center.clone(),
c.size,
Glyph::Flow(crate::preview::mermaid::flowchart::Shape::Rect),
)
})
};
let (Some((ac, asz, ag)), Some((bc, bsz, bg))) = (ends(&edge.from), ends(&edge.to)) else {
continue;
};
let straight = vec![
shapes::intersect(ag, ac.clone(), asz, &bc),
shapes::intersect(bg, bc.clone(), bsz, &ac),
];
let blocked = nodes
.iter()
.any(|n| n.id != edge.from && n.id != edge.to && crosses(&straight, n));
let (points, elbow) = if blocked {
(detour(&ac, asz, &bc, bsz), true)
} else {
(straight, false)
};
let (tip_start, tip_end) = edges::Tip::of_arrow(edge.arrow);
let label = edge.label.as_deref().map(Label::measure).and_then(|l| {
edges::arc_midpoint(&points).map(|center| PlacedEdgeLabel {
center,
size: Size::new(
l.width + super::LABEL_PAD_X * 2.0,
l.height + super::LABEL_PAD_Y * 2.0,
),
label: l,
})
});
placed.push(PlacedEdge {
from: edge.from.clone(),
to: edge.to.clone(),
points,
tip_start,
tip_end,
stroke: edge.stroke,
label,
start_label: None,
end_label: None,
badge: None,
series: None,
straight: elbow,
overlay: false,
});
}
clusters.sort_by_key(|c| c.depth);
let mut out = Diagram {
nodes,
edges: placed,
clusters,
..Diagram::default()
};
band::add_title(&mut out, diagram.preamble.title.as_deref());
normalise(&mut out);
Ok(out)
}
fn place(
items: &[Item],
columns: i32,
depth: usize,
x0: f64,
y0: f64,
nodes: &mut Vec<PlacedNode>,
clusters: &mut Vec<PlacedCluster>,
) -> Size {
let total: usize = items.iter().map(Item::width).sum();
let per_row = if columns == AUTO {
total.max(1)
} else {
(columns.max(1) as usize).max(1)
};
let mut sizes: Vec<Size> = Vec::with_capacity(items.len());
let mut inner: Vec<Vec<PlacedNode>> = Vec::with_capacity(items.len());
let mut inner_frames: Vec<Vec<PlacedCluster>> = Vec::with_capacity(items.len());
for item in items {
match item {
Item::Node(n) => sizes.push(node_size(n)),
Item::Space { .. } => sizes.push(Size::new(0.0, 0.0)),
Item::Composite(c) => {
let mut sub_nodes = Vec::new();
let mut sub_frames = Vec::new();
let size = place(
&c.children,
c.columns,
depth + 1,
0.0,
0.0,
&mut sub_nodes,
&mut sub_frames,
);
sizes.push(Size::new(
size.w + FRAME_PAD * 2.0,
size.h + FRAME_PAD * 2.0 + band::title_room(composite_title(c)),
));
inner.push(sub_nodes);
inner_frames.push(sub_frames);
continue;
}
}
inner.push(Vec::new());
inner_frames.push(Vec::new());
}
let cell_w = sizes
.iter()
.zip(items)
.map(|(s, i)| s.w / i.width().max(1) as f64)
.fold(MIN_CELL, f64::max);
let mut of_row: Vec<(usize, usize)> = Vec::with_capacity(items.len()); let mut col = 0usize;
let mut row = 0usize;
for item in items {
let span = item.width().max(1).min(per_row);
if col + span > per_row && col > 0 {
col = 0;
row += 1;
}
of_row.push((row, col));
col += span;
}
let rows = row + 1;
let mut row_h: Vec<f64> = vec![0.0; rows];
for (i, (r, _)) in of_row.iter().enumerate() {
row_h[*r] = row_h[*r].max(sizes[i].h);
}
for h in &mut row_h {
*h = h.max(MIN_ROW);
}
let row_top: Vec<f64> = (0..rows)
.map(|r| y0 + row_h[..r].iter().sum::<f64>() + CELL_GAP * r as f64)
.collect();
let mut widest = 0.0_f64;
for (i, item) in items.iter().enumerate() {
let (r, c) = of_row[i];
let span = item.width().max(1).min(per_row);
let w = cell_w * span as f64 + CELL_GAP * (span - 1) as f64;
let x = x0 + c as f64 * (cell_w + CELL_GAP);
let center = Point::new(x + w / 2.0, row_top[r] + row_h[r] / 2.0);
widest = widest.max(x + w - x0);
match item {
Item::Space { .. } => {}
Item::Node(n) => {
let natural = sizes[i];
let (bw, bh) = if keeps_its_shape(n) {
(natural.w, natural.h)
} else {
(w, natural.h.max(row_h[r] * 0.8).min(row_h[r]))
};
nodes.push(block_node(n, center, Size::new(bw, bh)));
}
Item::Composite(c2) => {
let dx = center.x - sizes[i].w / 2.0 + FRAME_PAD;
let dy =
center.y - sizes[i].h / 2.0 + FRAME_PAD + band::title_room(composite_title(c2));
for mut n in inner[i].clone() {
n.center = Point::new(n.center.x + dx, n.center.y + dy);
nodes.push(n);
}
for mut f in inner_frames[i].clone() {
f.center = Point::new(f.center.x + dx, f.center.y + dy);
if f.parent.is_none() {
f.parent = Some(c2.id.clone());
}
clusters.push(f);
}
clusters.push(composite_frame(
c2,
center,
Size::new(w, sizes[i].h.max(row_h[r])),
depth,
));
}
}
}
Size::new(
widest.max(cell_w),
row_h.iter().sum::<f64>() + CELL_GAP * (rows - 1) as f64,
)
}
fn crosses(line: &[Point], node: &PlacedNode) -> bool {
let (l, t, r, b) = node.bounds();
let (a, z) = (&line[0], &line[line.len() - 1]);
let steps = ((a.x - z.x).hypot(a.y - z.y).ceil() as usize).clamp(1, 4000);
(0..=steps).any(|i| {
let f = i as f64 / steps as f64;
let (x, y) = (a.x + f * (z.x - a.x), a.y + f * (z.y - a.y));
x > l + 0.5 && x < r - 0.5 && y > t + 0.5 && y < b - 0.5
})
}
fn detour(a: &Point, asz: Size, z: &Point, zsz: Size) -> Vec<Point> {
let under = (a.y + asz.h / 2.0).max(z.y + zsz.h / 2.0) + DETOUR;
vec![
Point::new(a.x, a.y + asz.h / 2.0),
Point::new(a.x, under),
Point::new(z.x, under),
Point::new(z.x, z.y + zsz.h / 2.0),
]
}
fn block_node(n: &Node, center: Point, size: Size) -> PlacedNode {
let label = Label::measure(&n.label);
let (glyph, mark) = match &n.arrow {
Some(dirs) => (
Glyph::BlockArrow,
Some(Mark::BlockArrow {
left: dirs.iter().any(|d| matches!(d, Dir::Left | Dir::X)),
right: dirs.iter().any(|d| matches!(d, Dir::Right | Dir::X)),
up: dirs.iter().any(|d| matches!(d, Dir::Up | Dir::Y)),
down: dirs.iter().any(|d| matches!(d, Dir::Down | Dir::Y)),
}),
),
None => (Glyph::Flow(n.shape), None),
};
PlacedNode {
id: n.id.clone(),
shape: glyph,
center,
size,
label,
panel: None,
series: None,
mark,
}
}
fn node_size(n: &Node) -> Size {
let label = Label::measure(&n.label);
let glyph = if n.arrow.is_some() {
Glyph::BlockArrow
} else {
Glyph::Flow(n.shape)
};
let size = shapes::size(glyph, Size::new(label.width, label.height));
Size::new(
size.w / n.width.max(1) as f64 * n.width.max(1) as f64,
size.h,
)
}
fn composite_title(c: &Composite) -> &str {
if c.named {
c.id.as_str()
} else {
""
}
}
fn composite_frame(c: &Composite, center: Point, size: Size, depth: usize) -> PlacedCluster {
let mut frame = band::frame(
c.id.clone(),
"",
center.x - size.w / 2.0,
center.y - size.h / 2.0,
center.x + size.w / 2.0,
center.y + size.h / 2.0,
);
frame.title = Label::measure(composite_title(c));
frame.depth = depth;
frame
}
fn keeps_its_shape(n: &Node) -> bool {
use crate::preview::mermaid::flowchart::Shape;
n.arrow.is_some()
|| matches!(
n.shape,
Shape::Circle | Shape::DoubleCircle | Shape::Diamond | Shape::Hexagon
)
}