use std::collections::{BTreeMap, HashMap};
use crate::preview::mermaid::architecture::{Architecture, Side};
use crate::preview::mermaid::flowchart::{Shape, Stroke};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics;
use super::band;
use super::{
normalise, shapes, svg, Curve, Diagram, Glyph, Label, PlacedCluster, PlacedEdge, PlacedNode,
RenderError, Size, Theme, Tip,
};
pub const CELL_W: f64 = 132.0;
pub const CELL_H: f64 = 84.0;
pub const CELL_GAP: f64 = 20.0;
pub const GROUP_PAD: f64 = 12.0;
pub const JUNCTION: f64 = 8.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let arch = crate::preview::mermaid::architecture::parse(code)?;
let diagram = lay_out(&arch)?;
Ok(svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(arch: &Architecture) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if arch.services.is_empty() {
return Err(RenderError::NothingToDraw);
}
let cells = place_on_grid(arch);
let boxes: Vec<(Glyph, Label, Size)> = arch.services.iter().map(measure).collect();
let sizes: Vec<Size> = boxes.iter().map(|(_, _, size)| *size).collect();
let xs = axis(&cells, &sizes, true);
let ys = axis(&cells, &sizes, false);
let mut nodes: Vec<PlacedNode> = Vec::new();
for (i, s) in arch.services.iter().enumerate() {
let (col, row) = cells[i];
let center = Point::new(xs[&col], ys[&row]);
let (glyph, label, size) = boxes[i].clone();
nodes.push(PlacedNode {
id: s.id.clone(),
shape: glyph,
center,
size,
label,
panel: None,
series: None,
mark: None,
style: None,
});
let _ = s;
}
let by_id: HashMap<&str, usize> = arch
.services
.iter()
.enumerate()
.map(|(i, s)| (s.id.as_str(), i))
.collect();
let mut edges: Vec<PlacedEdge> = Vec::new();
for e in &arch.edges {
let (Some(&a), Some(&b)) = (by_id.get(e.from.as_str()), by_id.get(e.to.as_str())) else {
continue;
};
let (from, to) = (&nodes[a], &nodes[b]);
let start = port(from, e.from_side);
let end = port(to, e.to_side);
let mut points = vec![start.clone()];
if (start.x - end.x).abs() > 0.5 && (start.y - end.y).abs() > 0.5 {
points.push(match e.from_side {
Side::Left | Side::Right => Point::new(end.x, start.y),
Side::Top | Side::Bottom => Point::new(start.x, end.y),
});
}
points.push(end);
edges.push(PlacedEdge {
from: e.from.clone(),
to: e.to.clone(),
points,
gaps: Vec::new(),
tip_start: if e.from_arrow { Tip::Arrow } else { Tip::None },
tip_end: if e.to_arrow { Tip::Arrow } else { Tip::None },
stroke: Stroke::Normal,
label: None,
start_label: None,
end_label: None,
badge: None,
series: None,
straight: true,
overlay: false,
style: None,
curve: Curve::Basis,
tip_matches_line: false,
});
if !e.title.trim().is_empty() {
let label = Label::measure(&e.title);
let mid = edges.last().expect("just pushed").points.clone();
if let Some(center) = super::edges::arc_midpoint(&mid) {
let size = Size::new(
label.width + super::LABEL_PAD_X * 2.0,
label.height + super::LABEL_PAD_Y * 2.0,
);
edges.last_mut().expect("just pushed").label = Some(super::PlacedEdgeLabel {
center,
size,
label,
});
}
}
}
let mut clusters: Vec<PlacedCluster> = Vec::new();
let mut order: Vec<usize> = (0..arch.groups.len()).collect();
order.sort_by_key(|i| std::cmp::Reverse(depth_of(arch, &arch.groups[*i].id)));
for gi in order {
let group = &arch.groups[gi];
let (mut l, mut t, mut r, mut b) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
let mut grow = |(ml, mt, mr, mb): (f64, f64, f64, f64)| {
l = l.min(ml);
t = t.min(mt);
r = r.max(mr);
b = b.max(mb);
};
for (i, s) in arch.services.iter().enumerate() {
if s.group.as_deref() == Some(group.id.as_str()) {
grow(nodes[i].bounds());
}
}
for c in &clusters {
if c.parent.as_deref() == Some(group.id.as_str()) {
grow(c.bounds());
}
}
if l > r {
continue;
}
let mut frame = band::frame(
group.id.clone(),
&group.title,
l - GROUP_PAD,
t - GROUP_PAD,
r + GROUP_PAD,
b + GROUP_PAD,
);
frame.parent = group.parent.clone();
frame.depth = depth_of(arch, &group.id);
clusters.push(frame);
}
clusters.sort_by_key(|c| c.depth);
let mut diagram = Diagram {
nodes,
edges,
clusters,
..Diagram::default()
};
band::add_title(&mut diagram, arch.preamble.title.as_deref());
normalise(&mut diagram);
Ok(diagram)
}
fn depth_of(arch: &Architecture, id: &str) -> usize {
let mut at = arch
.groups
.iter()
.find(|g| g.id == id)
.and_then(|g| g.parent.as_deref());
let mut n = 0usize;
for _ in 0..arch.groups.len() + 1 {
let Some(parent) = at else { break };
n += 1;
at = arch
.groups
.iter()
.find(|g| g.id == parent)
.and_then(|g| g.parent.as_deref());
}
n
}
fn port(node: &PlacedNode, side: Side) -> Point {
let (l, t, r, b) = node.bounds();
match side {
Side::Left => Point::new(l, node.center.y),
Side::Right => Point::new(r, node.center.y),
Side::Top => Point::new(node.center.x, t),
Side::Bottom => Point::new(node.center.x, b),
}
}
fn measure(s: &crate::preview::mermaid::architecture::Service) -> (Glyph, Label, Size) {
if s.junction {
return (
Glyph::ChartPoint,
Label::measure(""),
Size::new(JUNCTION, JUNCTION),
);
}
let text = if s.title.trim().is_empty() {
s.id.clone()
} else {
s.title.clone()
};
let label = Label::measure(&text);
let glyph = Glyph::Flow(Shape::RoundedRect);
let size = shapes::size(glyph, Size::new(label.width, label.height));
(glyph, label, size)
}
fn axis(cells: &[(i32, i32)], sizes: &[Size], across: bool) -> HashMap<i32, f64> {
let pitch = if across { CELL_W } else { CELL_H };
let extent = |s: &Size| if across { s.w } else { s.h };
let key = |c: &(i32, i32)| if across { c.0 } else { c.1 };
let mut widest: BTreeMap<i32, f64> = BTreeMap::new();
for (i, c) in cells.iter().enumerate() {
let at = widest.entry(key(c)).or_insert(0.0);
*at = at.max(extent(&sizes[i]));
}
let mut out: HashMap<i32, f64> = HashMap::new();
let mut coordinate = 0.0_f64;
let mut previous: Option<(i32, f64)> = None;
for (&k, &size) in &widest {
if let Some((last_k, last_size)) = previous {
let steps = (k - last_k) as f64;
coordinate += (pitch * steps).max((last_size + size) / 2.0 + CELL_GAP);
}
out.insert(k, coordinate);
previous = Some((k, size));
}
out
}
fn place_on_grid(arch: &Architecture) -> Vec<(i32, i32)> {
let n = arch.services.len();
let index: HashMap<&str, usize> = arch
.services
.iter()
.enumerate()
.map(|(i, s)| (s.id.as_str(), i))
.collect();
let mut cell: Vec<Option<(i32, i32)>> = vec![None; n];
let mut taken: Vec<(i32, i32)> = Vec::new();
let mut next_free_row = 0i32;
let mut bands: Vec<Vec<usize>> = Vec::new();
for group in &arch.groups {
let members: Vec<usize> = (0..n)
.filter(|i| arch.services[*i].group.as_deref() == Some(group.id.as_str()))
.collect();
if !members.is_empty() {
bands.push(members);
}
}
let loose: Vec<usize> = (0..n)
.filter(|i| {
arch.services[*i]
.group
.as_deref()
.is_none_or(|g| !arch.groups.iter().any(|x| x.id == g))
})
.collect();
if !loose.is_empty() {
bands.push(loose);
}
for (b, band) in bands.iter().enumerate() {
if b > 0 {
next_free_row += 1;
}
let base_row = next_free_row;
let mut next_seed_col = 0i32;
for &seed in band {
if cell[seed].is_some() {
continue;
}
let mut at = (next_seed_col, base_row);
while taken.contains(&at) {
at.0 += 1;
}
next_seed_col = at.0 + 1;
taken.push(at);
cell[seed] = Some(at);
next_free_row = next_free_row.max(at.1 + 1);
let mut queue = vec![seed];
while let Some(from) = queue.pop() {
let here = cell[from].expect("placed");
for e in &arch.edges {
for (a, b, side) in [(&e.from, &e.to, e.from_side), (&e.to, &e.from, e.to_side)]
{
if index.get(a.as_str()) != Some(&from) {
continue;
}
let Some(&other) = index.get(b.as_str()) else {
continue;
};
if cell[other].is_some() || !band.contains(&other) {
continue;
}
let step = side.step();
let got = claim((here.0 + step.0, here.1 + step.1), step, &mut taken, n);
cell[other] = Some(got);
next_free_row = next_free_row.max(got.1 + 1);
queue.push(other);
}
}
}
}
}
let mut out: Vec<(i32, i32)> = cell.iter().map(|c| c.unwrap_or((0, 0))).collect();
apply_alignments(arch, &index, &mut out);
out
}
fn claim(at: (i32, i32), step: (i32, i32), taken: &mut Vec<(i32, i32)>, n: usize) -> (i32, i32) {
let mut c = at;
for _ in 0..(n as i32 + 2) {
if !taken.contains(&c) {
break;
}
if step == (0, 0) {
c.0 += 1;
} else {
c = (c.0 + step.0, c.1 + step.1);
}
}
taken.push(c);
c
}
fn apply_alignments(arch: &Architecture, index: &HashMap<&str, usize>, cells: &mut [(i32, i32)]) {
for a in &arch.alignments {
let members: Vec<usize> = a
.members
.iter()
.filter_map(|m| index.get(m.as_str()).copied())
.collect();
let Some(&first) = members.first() else {
continue;
};
for &m in &members {
if a.row {
cells[m].1 = cells[first].1;
} else {
cells[m].0 = cells[first].0;
}
}
let mut used: Vec<i32> = Vec::new();
for &m in &members {
let axis = if a.row { 0 } else { 1 };
let mut v = if axis == 0 { cells[m].0 } else { cells[m].1 };
while used.contains(&v) {
v += 1;
}
used.push(v);
if axis == 0 {
cells[m].0 = v;
} else {
cells[m].1 = v;
}
}
}
}