use super::*;
use crate::architecture::{
ARCHITECTURE_CYTOSCAPE_CANVAS_LABEL_WIDTH_SCALE,
ARCHITECTURE_SERVICE_LABEL_BOTTOM_EXTENSION_PX,
architecture_create_text_compound_label_extra_bottom_px,
architecture_create_text_root_label_extra_bottom_px,
};
mod edges;
mod foreign_object;
mod geometry;
mod icons;
mod labels;
mod model;
mod nodes;
mod root;
mod settings;
mod viewport;
use self::edges::{ArchitectureEdgeRenderContext, push_architecture_edges};
use self::geometry::{GroupRect, GroupRectComputer, bounds_from_rect, extend_bounds};
use self::labels::{svg_line_plain_text, wrap_svg_words_to_lines};
use self::model::{ArchitectureModel, ArchitectureModelAccess};
use self::nodes::{
ArchitectureNodeRenderContext, push_architecture_groups,
push_architecture_services_and_junctions,
};
use self::root::{
ArchitectureRootOpenContext, architecture_a11y_nodes, push_architecture_root_open,
};
use self::settings::ArchitectureRenderSettings;
use self::viewport::{ArchitectureRootViewportContext, finalize_architecture_root_viewport};
fn timing_section<'a>(
enabled: bool,
dst: &'a mut std::time::Duration,
) -> Option<super::timing::TimingGuard<'a>> {
enabled.then(|| super::timing::TimingGuard::new(dst))
}
struct ArchitectureRenderRequest<'a, M: ArchitectureModelAccess> {
layout: &'a ArchitectureDiagramLayout,
model: &'a M,
effective_config: &'a serde_json::Value,
sanitize_config_opt: Option<&'a merman_core::MermaidConfig>,
options: &'a SvgRenderOptions,
}
struct ArchitectureTimingState<'a> {
enabled: bool,
timings: &'a mut super::timing::RenderTimings,
total_start: std::time::Instant,
}
pub(super) fn render_architecture_diagram_svg_typed_with_config(
layout: &ArchitectureDiagramLayout,
model: &merman_core::diagrams::architecture::ArchitectureDiagramRenderModel,
effective_config: &merman_core::MermaidConfig,
options: &SvgRenderOptions,
) -> Result<String> {
let timing_enabled = super::timing::render_timing_enabled();
let mut timings = super::timing::RenderTimings::default();
let total_start = std::time::Instant::now();
render_architecture_diagram_svg_with_model(
ArchitectureRenderRequest {
layout,
model,
effective_config: effective_config.as_value(),
sanitize_config_opt: Some(effective_config),
options,
},
ArchitectureTimingState {
enabled: timing_enabled,
timings: &mut timings,
total_start,
},
)
}
pub(super) fn render_architecture_diagram_svg(
layout: &ArchitectureDiagramLayout,
semantic: &serde_json::Value,
effective_config: &serde_json::Value,
options: &SvgRenderOptions,
) -> Result<String> {
let timing_enabled = super::timing::render_timing_enabled();
let mut timings = super::timing::RenderTimings::default();
let total_start = std::time::Instant::now();
let model: ArchitectureModel = {
let _g = timing_section(timing_enabled, &mut timings.deserialize_model);
crate::json::from_value_ref(semantic)?
};
render_architecture_diagram_svg_with_model(
ArchitectureRenderRequest {
layout,
model: &model,
effective_config,
sanitize_config_opt: None,
options,
},
ArchitectureTimingState {
enabled: timing_enabled,
timings: &mut timings,
total_start,
},
)
}
pub(super) fn render_architecture_diagram_svg_with_config(
layout: &ArchitectureDiagramLayout,
semantic: &serde_json::Value,
effective_config: &merman_core::MermaidConfig,
options: &SvgRenderOptions,
) -> Result<String> {
let timing_enabled = super::timing::render_timing_enabled();
let mut timings = super::timing::RenderTimings::default();
let total_start = std::time::Instant::now();
let model: ArchitectureModel = {
let _g = timing_section(timing_enabled, &mut timings.deserialize_model);
crate::json::from_value_ref(semantic)?
};
render_architecture_diagram_svg_with_model(
ArchitectureRenderRequest {
layout,
model: &model,
effective_config: effective_config.as_value(),
sanitize_config_opt: Some(effective_config),
options,
},
ArchitectureTimingState {
enabled: timing_enabled,
timings: &mut timings,
total_start,
},
)
}
fn render_architecture_diagram_svg_with_model<M: ArchitectureModelAccess>(
req: ArchitectureRenderRequest<'_, M>,
timing: ArchitectureTimingState<'_>,
) -> Result<String> {
let ArchitectureRenderRequest {
layout,
model,
effective_config,
sanitize_config_opt,
options,
} = req;
let ArchitectureTimingState {
enabled: timing_enabled,
timings,
total_start,
} = timing;
fn section<'a>(
enabled: bool,
dst: &'a mut std::time::Duration,
) -> Option<super::timing::TimingGuard<'a>> {
enabled.then(|| super::timing::TimingGuard::new(dst))
}
let _g_render_svg = section(timing_enabled, &mut timings.render_svg);
let diagram_id = options.diagram_id.as_deref().unwrap_or("architecture");
let settings = ArchitectureRenderSettings::from_config(diagram_id, effective_config);
let ArchitectureRenderSettings {
css,
icon_size_px,
half_icon,
padding_px,
arch_font_size_px,
svg_font_size_px,
use_max_width,
text_style,
compound_text_style,
} = settings.clone();
let sanitize_config_owned: merman_core::MermaidConfig;
let sanitize_config = match sanitize_config_opt {
Some(cfg) => cfg,
None => {
sanitize_config_owned =
merman_core::MermaidConfig::from_value(effective_config.clone());
&sanitize_config_owned
}
};
let mut node_xy: rustc_hash::FxHashMap<&str, (f64, f64)> = rustc_hash::FxHashMap::default();
for n in &layout.nodes {
node_xy.insert(n.id.as_str(), (n.x, n.y));
}
let text_measurer = crate::text::VendoredFontMetricsTextMeasurer::default();
let a11y = architecture_a11y_nodes(diagram_id, model.acc_title(), model.acc_descr());
let mut content_bounds: Option<Bounds> = None;
let groups_len = model.groups_len();
let edges_len = model.edges_len();
let service_count = model.services().count();
let junction_count = model.junctions().count();
let singleton_icon_text_service_id =
if groups_len == 0 && service_count == 1 && junction_count == 0 && edges_len == 0 {
model.services().next().and_then(|service| {
if service.in_group.is_none()
&& service
.icon_text
.map(str::trim)
.is_some_and(|text: &str| !text.is_empty())
{
Some(service.id)
} else {
None
}
})
} else {
None
};
let singleton_icon_text_offset_y = |service_id: &str| {
if singleton_icon_text_service_id == Some(service_id) {
ARCHITECTURE_SERVICE_LABEL_BOTTOM_EXTENSION_PX
} else {
0.0
}
};
let mut service_bounds: rustc_hash::FxHashMap<&str, Bounds> = rustc_hash::FxHashMap::default();
for svc in model.services() {
let (x, y) = node_xy.get(svc.id).copied().unwrap_or((0.0, 0.0));
let y = y + singleton_icon_text_offset_y(svc.id);
let b_icon = bounds_from_rect(x, y, icon_size_px, icon_size_px);
let mut b_full = b_icon.clone();
if let Some(title) = svc.title.map(str::trim).filter(|t| !t.is_empty()) {
let lines =
wrap_svg_words_to_lines(title, icon_size_px * 1.5, &text_measurer, &text_style);
let mut bbox_left_root = 0.0f64;
let mut bbox_right_root = 0.0f64;
for line in &lines {
let s = svg_line_plain_text(line);
let (l, r) = text_measurer.measure_svg_text_bbox_x(s.as_str(), &text_style);
bbox_left_root = bbox_left_root.max(l);
bbox_right_root = bbox_right_root.max(r);
}
let line_count_root = lines.len().max(1);
let label_extra_bottom_root = architecture_create_text_root_label_extra_bottom_px(
svg_font_size_px,
line_count_root,
);
let (bbox_left_compound, bbox_right_compound) = {
let s = title;
let m = text_measurer.measure(s, &compound_text_style);
let mut half =
(m.width.max(0.0) * ARCHITECTURE_CYTOSCAPE_CANVAS_LABEL_WIDTH_SCALE) / 2.0;
half = (half * 2.0).round() / 2.0;
(half, half)
};
let label_extra_bottom_compound =
architecture_create_text_compound_label_extra_bottom_px(arch_font_size_px);
let cx = x + icon_size_px / 2.0;
let text_left_root = cx - bbox_left_root;
let text_right_root = cx + bbox_right_root;
let text_bottom_root = y + icon_size_px + label_extra_bottom_root;
let text_left_compound = cx - bbox_left_compound;
let text_right_compound = cx + bbox_right_compound;
let text_bottom_compound = y + icon_size_px + label_extra_bottom_compound;
let b_compound = Bounds {
min_x: b_full.min_x.min(text_left_compound),
min_y: b_full.min_y,
max_x: b_full.max_x.max(text_right_compound),
max_y: b_full.max_y.max(text_bottom_compound),
};
let b_root = Bounds {
min_x: b_full.min_x.min(text_left_root),
min_y: b_full.min_y,
max_x: b_full.max_x.max(text_right_root),
max_y: b_full.max_y.max(text_bottom_root),
};
b_full = if svc.in_group.is_some() {
b_compound
} else {
b_root
};
}
service_bounds.insert(svc.id, b_full.clone());
if svc.in_group.is_none() {
extend_bounds(&mut content_bounds, b_full);
} else {
extend_bounds(&mut content_bounds, b_icon);
}
}
let mut junction_bounds: rustc_hash::FxHashMap<&str, Bounds> = rustc_hash::FxHashMap::default();
for junction in model.junctions() {
let (x, y) = node_xy.get(junction.id).copied().unwrap_or((0.0, 0.0));
let b = bounds_from_rect(x, y, icon_size_px, icon_size_px);
junction_bounds.insert(junction.id, b.clone());
extend_bounds(&mut content_bounds, b);
}
let mut child_groups: rustc_hash::FxHashMap<&str, Vec<&str>> = rustc_hash::FxHashMap::default();
for g in model.groups() {
if let Some(parent) = g.in_group {
child_groups.entry(parent).or_default().push(g.id);
}
}
for v in child_groups.values_mut() {
v.sort_unstable();
}
let mut services_in_group: rustc_hash::FxHashMap<&str, Vec<&str>> =
rustc_hash::FxHashMap::default();
for svc in model.services() {
if let Some(parent) = svc.in_group {
services_in_group.entry(parent).or_default().push(svc.id);
}
}
for v in services_in_group.values_mut() {
v.sort_unstable();
}
let mut junctions_in_group: rustc_hash::FxHashMap<&str, Vec<&str>> =
rustc_hash::FxHashMap::default();
for junction in model.junctions() {
if let Some(parent) = junction.in_group {
junctions_in_group
.entry(parent)
.or_default()
.push(junction.id);
}
}
for v in junctions_in_group.values_mut() {
v.sort_unstable();
}
let mut group_rects_computer = GroupRectComputer::new(
icon_size_px,
&services_in_group,
&junctions_in_group,
&child_groups,
&service_bounds,
&junction_bounds,
);
for g in model.groups() {
let _ = group_rects_computer.compute(g.id);
}
let mut group_rects: Vec<GroupRect<'_>> = Vec::with_capacity(model.groups_len());
for g in model.groups() {
if let Some(b) = group_rects_computer.get(g.id) {
group_rects.push(GroupRect {
id: g.id,
x: b.min_x,
y: b.min_y,
w: (b.max_x - b.min_x).max(1.0),
h: (b.max_y - b.min_y).max(1.0),
icon: g.icon,
title: g.title,
});
extend_bounds(&mut content_bounds, b.clone());
}
}
let is_empty = service_count == 0
&& junction_count == 0
&& model.groups_len() == 0
&& model.edges_len() == 0;
let mut out = String::new();
push_architecture_root_open(ArchitectureRootOpenContext {
out: &mut out,
diagram_id,
css: css.as_str(),
a11y: &a11y,
is_empty,
use_max_width,
half_icon,
icon_size_px,
});
{
let mut edge_render_ctx = ArchitectureEdgeRenderContext {
out: &mut out,
layout,
model,
node_xy: &node_xy,
settings: &settings,
text_measurer: &text_measurer,
content_bounds: &mut content_bounds,
junction_bounds: &junction_bounds,
};
push_architecture_edges(&mut edge_render_ctx);
}
out.push_str("</g>");
{
let mut node_render_ctx = ArchitectureNodeRenderContext {
out: &mut out,
model,
node_xy: &node_xy,
settings: &settings,
text_measurer: &text_measurer,
sanitize_config,
content_bounds: &mut content_bounds,
singleton_icon_text_service_id,
};
push_architecture_services_and_junctions(&mut node_render_ctx);
push_architecture_groups(&mut node_render_ctx, &group_rects);
}
out.push_str("</svg>\n");
if !is_empty {
out = finalize_architecture_root_viewport(ArchitectureRootViewportContext {
out,
diagram_id,
model,
content_bounds,
padding_px,
icon_size_px,
use_max_width,
});
}
drop(_g_render_svg);
timings.total = total_start.elapsed();
if timing_enabled {
eprintln!(
"[render-timing] diagram=architecture total={:?} deserialize={:?} build_ctx={:?} viewbox={:?} render_svg={:?} finalize={:?}",
timings.total,
timings.deserialize_model,
timings.build_ctx,
timings.viewbox,
timings.render_svg,
timings.finalize_svg,
);
}
Ok(out)
}