use super::super::*;
use crate::architecture_metrics::architecture_estimate_service_bounds;
use crate::model::ArchitectureCytoscapeServiceBounds;
use super::edges::{ArchitectureEdgeRenderContext, push_architecture_edges};
use super::geometry::{GroupRect, GroupRectComputer, bounds_from_rect, extend_bounds};
use super::labels::{svg_line_plain_text, wrap_svg_words_to_lines};
use super::model::{ArchitectureModelAccess, ArchitectureServiceRef};
use super::nodes::{
ArchitectureNodeRenderContext, push_architecture_groups,
push_architecture_services_and_junctions,
};
use super::root::{architecture_a11y_nodes, begin_architecture_document};
use super::settings::ArchitectureRenderSettings;
use super::viewport::{ArchitectureRootViewportContext, finalize_architecture_root_viewport};
fn architecture_bounds_match_icon_rect(bounds: &Bounds, x: f64, y: f64, icon_size_px: f64) -> bool {
const EPSILON: f64 = 1e-6;
(bounds.min_x - x).abs() <= EPSILON
&& (bounds.min_y - y).abs() <= EPSILON
&& (bounds.max_x - (x + icon_size_px)).abs() <= EPSILON
&& (bounds.max_y - (y + icon_size_px)).abs() <= EPSILON
}
fn architecture_cached_service_child_bounds<'a>(
service_bounds_by_id: &'a rustc_hash::FxHashMap<&str, &'a ArchitectureCytoscapeServiceBounds>,
service: ArchitectureServiceRef<'_>,
x: f64,
y: f64,
icon_size_px: f64,
) -> Option<&'a ArchitectureCytoscapeServiceBounds> {
let cached = service_bounds_by_id.get(service.id).copied()?;
if cached.in_group.as_deref() != service.in_group {
return None;
}
if !architecture_bounds_match_icon_rect(&cached.body_bounds, x, y, icon_size_px) {
return None;
}
Some(cached)
}
fn architecture_svg_output_capacity<M: ArchitectureModelAccess>(
model: &M,
css_len: usize,
a11y_len: usize,
) -> usize {
let service_count = model.services().count();
let junction_count = model.junctions().count();
let group_count = model.groups_len();
let edge_count = model.edges_len();
1024usize
.saturating_add(css_len)
.saturating_add(a11y_len)
.saturating_add(service_count.saturating_mul(900))
.saturating_add(junction_count.saturating_mul(180))
.saturating_add(group_count.saturating_mul(700))
.saturating_add(edge_count.saturating_mul(650))
}
struct ArchitectureRenderRequest<'a, M: ArchitectureModelAccess> {
layout: &'a ArchitectureDiagramLayout,
model: &'a M,
effective_config: &'a serde_json::Value,
sanitize_config: &'a merman_core::MermaidConfig,
options: &'a SvgExecution<'a>,
}
struct ArchitectureTimingState<'a> {
timing: super::super::timing::RenderTiming,
timings: &'a mut super::super::timing::RenderTimings,
total_timer: Option<merman_core::runtime::OperationTimer>,
}
pub(crate) fn render_architecture_diagram_svg_typed_with_config(
layout: &ArchitectureDiagramLayout,
model: &merman_core::diagrams::architecture::ArchitectureDiagramRenderModel,
effective_config: &merman_core::MermaidConfig,
options: &SvgExecution<'_>,
) -> Result<root_svg::RootedSvg> {
let timing = options.timing();
let mut timings = super::super::timing::RenderTimings::default();
let total_timer = timing.start();
render_architecture_diagram_svg_with_model(
ArchitectureRenderRequest {
layout,
model,
effective_config: effective_config.as_value(),
sanitize_config: effective_config,
options,
},
ArchitectureTimingState {
timing,
timings: &mut timings,
total_timer,
},
)
}
fn render_architecture_diagram_svg_with_model<M: ArchitectureModelAccess>(
req: ArchitectureRenderRequest<'_, M>,
timing: ArchitectureTimingState<'_>,
) -> Result<root_svg::RootedSvg> {
let ArchitectureRenderRequest {
layout,
model,
effective_config,
sanitize_config,
options,
} = req;
let ArchitectureTimingState {
timing,
timings,
total_timer,
} = timing;
let _g_render_svg = timing.section(&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 css = settings.css.as_str();
let icon_size_px = settings.icon_size_px;
let half_icon = settings.half_icon;
let padding_px = settings.padding_px;
let arch_font_size_px = settings.arch_font_size_px;
let use_max_width = settings.use_max_width;
let text_style = &settings.text_style;
let compound_text_style = &settings.compound_text_style;
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 = options.text_measurer();
let a11y = architecture_a11y_nodes(diagram_id, model.acc_title(), model.acc_descr());
let mut content_bounds: Option<Bounds> = None;
let mut cached_service_bounds_by_id: rustc_hash::FxHashMap<
&str,
&ArchitectureCytoscapeServiceBounds,
> = rustc_hash::FxHashMap::default();
cached_service_bounds_by_id.reserve(layout.cytoscape_service_bounds.len());
for bounds in &layout.cytoscape_service_bounds {
cached_service_bounds_by_id.insert(bounds.id.as_str(), bounds);
}
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));
if svc.in_group.is_some()
&& let Some(cached) = architecture_cached_service_child_bounds(
&cached_service_bounds_by_id,
svc,
x,
y,
icon_size_px,
)
{
service_bounds.insert(svc.id, cached.union_bounds.clone());
extend_bounds(&mut content_bounds, cached.body_bounds.clone());
continue;
}
let estimate = architecture_estimate_service_bounds(
x,
y,
icon_size_px,
arch_font_size_px,
svc.title,
text_measurer,
text_style,
compound_text_style,
wrap_svg_words_to_lines,
|line| svg_line_plain_text(line.as_slice()),
|line, style| text_measurer.measure_svg_text_bbox_x(line, style),
);
let b_full = if svc.in_group.is_some() {
estimate
.cytoscape_group_child_contribution
.union_bounds
.clone()
} else {
estimate.svg_root_bounds.clone()
};
service_bounds.insert(svc.id, b_full.clone());
if svc.in_group.is_none() {
extend_bounds(&mut content_bounds, estimate.svg_root_bounds);
} else {
extend_bounds(&mut content_bounds, estimate.emitted_icon_bounds);
}
}
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,
padding_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 = model.services().next().is_none()
&& model.junctions().next().is_none()
&& model.groups_len() == 0
&& model.edges_len() == 0;
let mut out = String::with_capacity(architecture_svg_output_capacity(
model,
settings.css.len(),
a11y.nodes.len(),
));
let root_viewport = root_svg::RootViewportContext::new(
crate::family::RenderFamilyKind::Architecture,
diagram_id,
);
let root_document = begin_architecture_document(
&mut out,
&root_viewport,
diagram_id,
css,
&a11y,
use_max_width,
)?;
{
let mut edge_render_ctx = ArchitectureEdgeRenderContext {
out: &mut out,
diagram_id,
layout,
model,
node_xy: &node_xy,
settings: &settings,
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,
diagram_id,
model,
node_xy: &node_xy,
settings: &settings,
text_measurer,
sanitize_config,
icon_registry: options.icon_registry(),
work_meter: options.work_meter(),
content_bounds: &mut content_bounds,
};
push_architecture_services_and_junctions(&mut node_render_ctx)?;
push_architecture_groups(&mut node_render_ctx, &group_rects)?;
}
out.push_str("</svg>\n");
let rooted_svg = finalize_architecture_root_viewport(ArchitectureRootViewportContext {
out,
root_viewport: &root_viewport,
root_document,
content_bounds,
padding_px,
half_icon,
icon_size_px,
use_max_width,
is_empty,
trust_content_bounds: options.icon_registry().is_none(),
})?;
drop(_g_render_svg);
timings.total = total_timer
.map(merman_core::runtime::OperationTimer::elapsed)
.unwrap_or_default();
if timing.is_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(rooted_svg)
}