use crate::preview::mermaid::flowchart::{Shape, Stroke};
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::state::{self, Direction, Kind, NotePosition, StateDiagram};
use crate::preview::mermaid::text_metrics;
use super::edges::Tip;
use super::shapes::{self, Glyph, Size};
use super::svg;
use super::{
lay_out_spec, Curve, Diagram, GraphSpec, Label, PlacedEdge, PlacedNode, RenderError, SpecBlock,
SpecEdge, SpecNode, Theme,
};
pub const NOTE_GAP: f64 = 24.0;
const NOTE_PUSH_LIMIT: usize = 200;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let diagram = state::parse(code)?;
let laid = lay_out(&diagram)?;
Ok(svg::emit(&laid, &Theme::named(theme)))
}
pub fn lay_out(diagram: &StateDiagram) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if diagram.states.is_empty() {
return Err(RenderError::NothingToDraw);
}
let mut out = lay_out_spec(&spec_of(diagram))?;
place_notes(&mut out, diagram);
Ok(out)
}
pub fn spec_of(diagram: &StateDiagram) -> GraphSpec {
let horizontal_bars = matches!(
diagram.direction,
Direction::TopToBottom | Direction::BottomToTop
);
let mut nodes: Vec<SpecNode> = Vec::new();
let mut blocks: Vec<SpecBlock> = Vec::new();
for s in &diagram.states {
if s.kind == Kind::Note {
continue;
}
if s.kind.is_block() {
blocks.push(SpecBlock {
id: s.id.clone(),
title: s.label.clone(),
members: s.members.clone(),
dashed: s.kind == Kind::Concurrent,
});
continue;
}
let glyph = glyph_of(s.kind, s.titled, horizontal_bars);
let label = Label::measure(&s.label);
let size = shapes::size(glyph, shapes::Size::new(label.width, label.height));
nodes.push(SpecNode {
id: s.id.clone(),
glyph,
label,
size,
panel: None,
style: None,
});
}
let edges = diagram
.transitions
.iter()
.filter(|t| !t.is_note_link)
.map(|t| SpecEdge {
id: t.id.clone(),
from: t.from.clone(),
to: t.to.clone(),
label: t
.label
.as_deref()
.map(Label::measure)
.filter(|l| !l.is_blank()),
tip_start: Tip::None,
tip_end: Tip::Arrow,
stroke: Stroke::Normal,
minlen: 1,
start_label: None,
end_label: None,
style: None,
curve: Curve::Basis,
})
.collect();
GraphSpec {
direction: diagram.direction,
nodes,
edges,
blocks,
}
}
fn place_notes(out: &mut Diagram, model: &StateDiagram) {
for note in model.states.iter().filter(|s| s.kind == Kind::Note) {
let Some(position) = note.note_position else {
continue;
};
let Some(link) = model
.transitions
.iter()
.find(|t| t.is_note_link && (t.from == note.id || t.to == note.id))
else {
continue;
};
let anchor_id = if link.from == note.id {
link.to.clone()
} else {
link.from.clone()
};
let Some((anchor_center, anchor_size, anchor_glyph)) = out
.node(&anchor_id)
.map(|n| (n.center.clone(), n.size, n.shape))
.or_else(|| {
out.cluster(&anchor_id)
.map(|c| (c.center.clone(), c.size, Glyph::Flow(Shape::Rect)))
})
else {
continue;
};
let label = Label::measure(¬e.label);
let size = shapes::size(Glyph::Note, Size::new(label.width, label.height));
let sign = match position {
NotePosition::Right => 1.0,
NotePosition::Left => -1.0,
};
let mut center = Point::new(
anchor_center.x + sign * (anchor_size.w / 2.0 + NOTE_GAP + size.w / 2.0),
anchor_center.y,
);
center = Point::new(center.x, free_row(out, ¢er, size));
let note_point = shapes::intersect(Glyph::Note, center.clone(), size, &anchor_center);
let anchor_point = shapes::intersect(anchor_glyph, anchor_center, anchor_size, ¢er);
let points = if link.from == note.id {
vec![note_point, anchor_point]
} else {
vec![anchor_point, note_point]
};
out.nodes.push(PlacedNode {
id: note.id.clone(),
shape: Glyph::Note,
center,
size,
label,
panel: None,
series: None,
mark: None,
style: None,
});
out.edges.push(PlacedEdge {
from: link.from.clone(),
to: link.to.clone(),
points,
tip_start: Tip::None,
tip_end: Tip::None,
stroke: Stroke::Dotted,
label: None,
start_label: None,
end_label: None,
badge: None,
series: None,
straight: false,
overlay: false,
style: None,
curve: Curve::Basis,
});
}
super::normalise(out);
}
const SLIDE_STEPS: usize = 200;
fn free_row(out: &Diagram, center: &Point, size: Size) -> f64 {
let mut y = center.y;
for _ in 0..SLIDE_STEPS {
let Some(clear_by) = box_overlap(out, &Point::new(center.x, y), size) else {
break;
};
y += clear_by.max(0.0) + NOTE_GAP;
}
y
}
fn box_overlap(out: &Diagram, center: &Point, size: Size) -> Option<f64> {
let (l, t, r, b) = (
center.x - size.w / 2.0,
center.y - size.h / 2.0,
center.x + size.w / 2.0,
center.y + size.h / 2.0,
);
let on_a_box = out.nodes.iter().find_map(|n| {
let (nl, nt, nr, nb) = n.bounds();
let dx = nr.min(r) - nl.max(l);
let dy = nb.min(b) - nt.max(t);
(dx > 0.01 && dy > 0.01).then_some(dy)
});
if on_a_box.is_some() {
return on_a_box;
}
let mut lowest: Option<f64> = None;
for e in &out.edges {
for w in e.drawn_points().windows(2) {
let len = (w[1].x - w[0].x).hypot(w[1].y - w[0].y);
let steps = ((len * 2.0).ceil() as usize).clamp(1, 4000);
for i in 0..=steps {
let t_i = i as f64 / steps as f64;
let (x, y) = (
w[0].x + t_i * (w[1].x - w[0].x),
w[0].y + t_i * (w[1].y - w[0].y),
);
if x > l + 0.01 && x < r - 0.01 && y > t + 0.01 && y < b - 0.01 {
lowest = Some(lowest.map_or(y, |cur: f64| cur.max(y)));
}
}
}
}
lowest.map(|y| y - t)
}
fn glyph_of(kind: Kind, titled: bool, horizontal_bars: bool) -> Glyph {
match kind {
Kind::Start => Glyph::StateStart,
Kind::End => Glyph::StateEnd,
Kind::Choice => Glyph::Choice,
Kind::Fork | Kind::Join => Glyph::Bar {
horizontal: horizontal_bars,
},
Kind::Note => Glyph::Note,
Kind::Simple if titled => Glyph::TitledBox,
Kind::Simple => Glyph::Flow(Shape::RoundedRect),
Kind::Composite | Kind::Concurrent => Glyph::Flow(Shape::RoundedRect),
}
}