use crate::preview::mermaid::journey::Journey;
use crate::preview::mermaid::layout::Point;
use crate::preview::mermaid::text_metrics;
use super::band;
use super::chart::label_node;
use super::shapes::Mark;
use super::{
normalise, shapes, svg, Diagram, Glyph, Label, PlacedCluster, PlacedNode, RenderError, Size,
Theme,
};
pub const TASK_GAP: f64 = 14.0;
pub const SECTION_PAD: f64 = 6.0;
pub const FACE_GAP: f64 = 6.0;
pub const PEOPLE_GAP: f64 = 4.0;
pub const MIN_TASK_WIDTH: f64 = 64.0;
pub fn render(code: &str, theme: &str) -> Result<String, RenderError> {
let journey = crate::preview::mermaid::journey::parse(code)?;
let diagram = lay_out(&journey)?;
Ok(svg::emit(&diagram, &Theme::named(theme)))
}
pub fn lay_out(journey: &Journey) -> Result<Diagram, RenderError> {
if !text_metrics::fonts_available() {
return Err(RenderError::NoFonts);
}
if journey.tasks.is_empty() {
return Err(RenderError::NothingToDraw);
}
let face = shapes::FACE_RADIUS * 2.0;
let mut nodes: Vec<PlacedNode> = Vec::new();
let mut boxes: Vec<(usize, f64, f64)> = Vec::new();
let names: Vec<Label> = journey
.tasks
.iter()
.map(|t| Label::measure(&t.name))
.collect();
let people: Vec<Label> = journey
.tasks
.iter()
.map(|t| Label::measure(&t.people.join(", ")))
.collect();
let box_h = shapes::size(
Glyph::Flow(crate::preview::mermaid::flowchart::Shape::RoundedRect),
Size::new(0.0, names.iter().map(|l| l.height).fold(0.0_f64, f64::max)),
)
.h;
let face_top = 0.0_f64;
let box_top = face_top + face + FACE_GAP;
let people_top = box_top + box_h + PEOPLE_GAP;
let mut x = 0.0_f64;
for (i, task) in journey.tasks.iter().enumerate() {
let w = names[i]
.width
.max(people[i].width)
.max(face)
.max(MIN_TASK_WIDTH)
+ shapes::PADDING * 2.0;
let cx = x + w / 2.0;
if let Some(score) = task.score {
nodes.push(PlacedNode {
id: format!("task#{i}#face"),
shape: Glyph::Face,
center: Point::new(cx, face_top + face / 2.0),
size: Size::new(face, face),
label: Label::measure(""),
panel: None,
series: None,
mark: Some(Mark::Face { score }),
});
}
nodes.push(PlacedNode {
id: format!("task#{i}"),
shape: Glyph::Flow(crate::preview::mermaid::flowchart::Shape::RoundedRect),
center: Point::new(cx, box_top + box_h / 2.0),
size: Size::new(w, box_h),
label: names[i].clone(),
panel: None,
series: None,
mark: None,
});
if let Some(n) = label_node(
format!("task#{i}#people"),
people[i].clone(),
Point::new(cx, people_top + people[i].height / 2.0),
None,
) {
nodes.push(n);
}
boxes.push((i, x, x + w));
x += w + TASK_GAP;
}
let mut sections: Vec<PlacedCluster> = Vec::new();
let people_h = people.iter().map(|l| l.height).fold(0.0_f64, f64::max);
let bottom = people_top + people_h;
let mut run: Option<(String, f64, f64)> = None;
let mut ordinal = 0usize;
for (i, left, right) in &boxes {
let name = journey.tasks[*i].section.clone();
match &mut run {
Some((open, _, end)) if *open == name => *end = *right,
_ => {
if let Some((open, start, end)) = run.take() {
sections.push(section_frame(ordinal, &open, start, end, face_top, bottom));
ordinal += 1;
}
run = Some((name, *left, *right));
}
}
}
if let Some((open, start, end)) = run.take() {
sections.push(section_frame(ordinal, &open, start, end, face_top, bottom));
}
sections.retain(|s| !s.title.is_blank());
let mut diagram = Diagram {
nodes,
clusters: sections,
..Diagram::default()
};
band::add_title(&mut diagram, journey.preamble.title.as_deref());
normalise(&mut diagram);
Ok(diagram)
}
fn section_frame(
ordinal: usize,
title: &str,
left: f64,
right: f64,
top: f64,
bottom: f64,
) -> PlacedCluster {
band::frame(
format!("section#{ordinal}#{title}"),
title,
left - SECTION_PAD,
top - SECTION_PAD,
right + SECTION_PAD,
bottom + SECTION_PAD,
)
}