use super::*;
pub(in crate::svg::parity) fn render_state_diagram_svg_model(
layout: &StateDiagramLayout,
model: &StateSvgModel,
effective_config: &serde_json::Value,
diagram_title: Option<&str>,
measurer: &dyn TextMeasurer,
options: &SvgExecution<'_>,
) -> Result<root_svg::RootedSvg> {
let timing = options.timing();
let mut timings = super::timing::RenderTimings::default();
let total_timer = timing.start();
let diagram_id = options.diagram_id_or("merman");
let _g_build_ctx = timing.section(&mut timings.build_ctx);
let mut hidden_prefixes: Vec<String> = Vec::new();
for (id, st) in &model.states {
let Some(note) = st.note.as_ref() else {
continue;
};
if note.text.trim().is_empty() {
continue;
}
if note.position.is_none() {
hidden_prefixes.push(id.clone());
}
}
let viewport_padding = 8.0;
let origin_x = 0.0;
let origin_y = 0.0;
let diagram_title = diagram_title
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let state_render_settings =
crate::state::StateConfigView::new(effective_config).render_settings();
let title_top_margin = state_render_settings.title_top_margin;
let hand_drawn_seed = options.rough_randomness(
state_render_settings.hand_drawn_seed,
"render.state.roughjs",
);
let rough_cache = StateRoughCache::default();
let has_acc_title = model
.acc_title
.as_deref()
.is_some_and(|s| !s.trim().is_empty());
let has_acc_descr = model
.acc_descr
.as_deref()
.is_some_and(|s| !s.trim().is_empty());
let text_style = state_render_settings.text_style.clone();
let mut nodes_by_id: FxHashMap<&str, &StateSvgNode> =
FxHashMap::with_capacity_and_hasher(model.nodes.len(), Default::default());
for n in &model.nodes {
nodes_by_id.insert(n.id.as_str(), n);
}
let mut layout_nodes_by_id: FxHashMap<&str, &LayoutNode> =
FxHashMap::with_capacity_and_hasher(layout.nodes.len(), Default::default());
for n in &layout.nodes {
layout_nodes_by_id.insert(n.id.as_str(), n);
}
let mut layout_edges_by_id: FxHashMap<&str, &crate::model::LayoutEdge> =
FxHashMap::with_capacity_and_hasher(layout.edges.len(), Default::default());
for e in &layout.edges {
layout_edges_by_id.insert(e.id.as_str(), e);
}
let mut layout_clusters_by_id: FxHashMap<&str, &LayoutCluster> =
FxHashMap::with_capacity_and_hasher(layout.clusters.len(), Default::default());
for c in &layout.clusters {
layout_clusters_by_id.insert(c.id.as_str(), c);
}
let mut parent: FxHashMap<&str, &str> =
FxHashMap::with_capacity_and_hasher(model.nodes.len(), Default::default());
for n in &model.nodes {
if let Some(p) = n.parent_id.as_deref() {
parent.insert(n.id.as_str(), p);
}
}
let node_order: Vec<&str> = model.nodes.iter().map(|n| n.id.as_str()).collect();
let mut ctx = StateRenderCtx {
diagram_id,
diagram_look: state_render_settings.diagram_look,
hand_drawn_seed,
html_labels: state_render_settings.html_labels,
html_label_wrapping_width: state_render_settings.html_label_wrapping_width,
state_padding: state_render_settings.state_padding,
node_order,
nodes_by_id,
layout_nodes_by_id,
layout_edges_by_id,
layout_clusters_by_id,
parent,
nested_roots: std::collections::BTreeSet::new(),
hidden_prefixes,
security_level_loose: state_render_settings.security_level_loose,
links: &model.links,
states: &model.states,
edges: &model.edges,
include_edges: options.debug.include_edges,
include_nodes: options.debug.include_nodes,
measurer,
text_style,
theme_defaults: StateThemeDefaults::from_config(effective_config),
rough_cache,
};
fn compute_state_nested_roots(ctx: &StateRenderCtx<'_>) -> std::collections::BTreeSet<String> {
let mut out: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut composite_self_loops: std::collections::HashSet<&str> =
std::collections::HashSet::new();
for e in ctx.edges {
if state_is_hidden(ctx, e.start.as_str())
|| state_is_hidden(ctx, e.end.as_str())
|| state_is_hidden(ctx, e.id.as_str())
{
continue;
}
if e.start != e.end {
continue;
}
let id = e.start.as_str();
let Some(n) = ctx.nodes_by_id.get(id).copied() else {
continue;
};
if n.is_group && n.shape != "noteGroup" {
composite_self_loops.insert(id);
}
}
let mut composite_externals: std::collections::HashSet<&str> =
std::collections::HashSet::new();
for e in ctx.edges {
if state_is_hidden(ctx, e.start.as_str())
|| state_is_hidden(ctx, e.end.as_str())
|| state_is_hidden(ctx, e.id.as_str())
{
continue;
}
let a = state_endpoint_context_raw(ctx, e.start.as_str());
let b = state_endpoint_context_raw(ctx, e.end.as_str());
let ca = state_context_chain_raw(ctx, a);
let cb = state_context_chain_raw(ctx, b);
for anc in &ca {
let Some(id) = *anc else {
continue;
};
if cb.contains(anc) {
continue;
}
let Some(n) = ctx.nodes_by_id.get(id).copied() else {
continue;
};
if n.is_group && n.shape != "noteGroup" {
composite_externals.insert(id);
}
}
for anc in &cb {
let Some(id) = *anc else {
continue;
};
if ca.contains(anc) {
continue;
}
let Some(n) = ctx.nodes_by_id.get(id).copied() else {
continue;
};
if n.is_group && n.shape != "noteGroup" {
composite_externals.insert(id);
}
}
}
for e in ctx.edges {
if state_is_hidden(ctx, e.start.as_str())
|| state_is_hidden(ctx, e.end.as_str())
|| state_is_hidden(ctx, e.id.as_str())
{
continue;
}
if composite_self_loops.contains(e.start.as_str()) && e.start == e.end {
continue;
}
let Some(c) = state_edge_context_raw(ctx, e) else {
continue;
};
if composite_externals.contains(c) {
continue;
}
out.insert(c.to_string());
}
for (child_id, parent_id) in &ctx.parent {
if state_is_hidden(ctx, child_id) || state_is_hidden(ctx, parent_id) {
continue;
}
if composite_self_loops.contains(parent_id) {
continue;
}
if composite_externals.contains(parent_id) {
continue;
}
let Some(pn) = ctx.nodes_by_id.get(parent_id).copied() else {
continue;
};
if pn.is_group && pn.shape != "noteGroup" {
out.insert((*parent_id).to_string());
}
}
let seeds: Vec<String> = out.iter().cloned().collect();
for cid in seeds {
let mut cur: Option<&str> = Some(cid.as_str());
while let Some(id) = cur {
let Some(pid) = ctx.parent.get(id).copied() else {
break;
};
let Some(pn) = ctx.nodes_by_id.get(pid).copied() else {
cur = Some(pid);
continue;
};
if pn.is_group && pn.shape != "noteGroup" {
if composite_self_loops.contains(pid) || composite_externals.contains(pid) {
cur = Some(pid);
continue;
}
out.insert(pid.to_string());
}
cur = Some(pid);
}
}
out
}
ctx.nested_roots = compute_state_nested_roots(&ctx);
drop(_g_build_ctx);
let _g_render_svg = timing.section(&mut timings.render_svg);
const TITLE_PLACEHOLDER_COMMENT: &str = "<!--__MERMAID_TITLE__-->";
let css = state_css(diagram_id, model, effective_config);
options.checkpoint_emit()?;
let estimated_svg_bytes = 2048usize
+ css.len()
+ layout.nodes.len().saturating_mul(512)
+ layout.edges.len().saturating_mul(384)
+ layout.clusters.len().saturating_mul(256);
let mut out = String::with_capacity(estimated_svg_bytes);
let aria_labelledby = has_acc_title.then(|| format!("chart-title-{diagram_id}"));
let aria_describedby = has_acc_descr.then(|| format!("chart-desc-{diagram_id}"));
let root_context =
root_svg::RootViewportContext::new(crate::family::RenderFamilyKind::State, diagram_id);
let mut root_chrome = root_svg::RootChrome::new(diagram_id, "stateDiagram");
root_chrome.class = Some("statediagram");
root_chrome.aria_labelledby = aria_labelledby.as_deref();
root_chrome.aria_describedby = aria_describedby.as_deref();
root_chrome.dom = root_svg::RootDomProfile {
aria_attr_order: root_svg::SvgRootAriaAttrOrder::LabelledbyThenDescribedby,
trailing_newline: false,
..root_svg::RootDomProfile::default()
};
let root_document = root_context.begin_document(
&mut out,
root_svg::DeferredRootSpec::responsive(),
root_chrome,
)?;
options.checkpoint_emit()?;
if has_acc_title {
let _ = write!(
&mut out,
r#"<title id="chart-title-{}">{}"#,
diagram_id,
escape_xml_display(model.acc_title.as_deref().unwrap_or_default())
);
out.push_str("</title>");
}
if has_acc_descr {
let _ = write!(
&mut out,
r#"<desc id="chart-desc-{}">{}"#,
diagram_id,
escape_xml_display(model.acc_descr.as_deref().unwrap_or_default())
);
out.push_str("</desc>");
}
let _ = write!(&mut out, "<style>{}</style>", css);
out.push_str("<g>");
state_markers(&mut out, diagram_id, effective_config);
options.checkpoint_emit()?;
let bounds_scan_start = out.len();
let mut detail = StateRenderDetails::default();
render_state_root(
&mut out,
&ctx,
None,
(origin_x, origin_y),
options,
timing,
&mut detail,
)?;
let bounds_scan_end = out.len();
out.push_str("</g>");
state_root_defs(&mut out, diagram_id, effective_config);
options.checkpoint_emit()?;
out.push_str(TITLE_PLACEHOLDER_COMMENT);
out.push_str("</svg>\n");
drop(_g_render_svg);
let mut viewbox_svg_scan = std::time::Duration::ZERO;
let _g_viewbox = timing.section(&mut timings.viewbox);
let _g_scan = timing.section(&mut viewbox_svg_scan);
let mut content_bounds = svg_emitted_bounds_from_svg(&out[bounds_scan_start..bounds_scan_end])
.or_else(|| state_viewport_bounds_from_layout(layout))
.unwrap_or(Bounds {
min_x: 0.0,
min_y: 0.0,
max_x: 100.0,
max_y: 100.0,
});
drop(_g_scan);
let mut title_svg = String::new();
if let Some(title) = diagram_title.as_deref() {
let title_x = (content_bounds.min_x + content_bounds.max_x) / 2.0;
let title_y = -title_top_margin;
let mut title_style = crate::state::state_text_style(effective_config);
title_style.font_size = 18.0;
let (title_left, title_right) = measurer.measure_svg_title_bbox_x(title, &title_style);
let (ascent, descent) = crate::text::svg_title_bbox_vertical_extents_px(&title_style);
content_bounds.min_x = content_bounds.min_x.min(title_x - title_left);
content_bounds.max_x = content_bounds.max_x.max(title_x + title_right);
content_bounds.min_y = content_bounds.min_y.min(title_y - ascent);
content_bounds.max_y = content_bounds.max_y.max(title_y + descent);
title_svg = String::with_capacity(title.len() + 128);
let _ = write!(
&mut title_svg,
r#"<text text-anchor="middle" x="{}" y="{}" class="statediagramTitleText">{}</text>"#,
fmt(title_x),
fmt(title_y),
escape_xml_display(title)
);
}
let root_bounds = root_svg::DiagramBounds::from_extents(
content_bounds.min_x,
content_bounds.min_y,
content_bounds.max_x,
content_bounds.max_y,
viewport_padding,
);
let root_document = root_context.finish_document(
&mut out,
root_document,
root_svg::RootViewportSpec::responsive(root_bounds)
.with_max_width(root_svg::RootMaxWidth::CssSixSignificant(root_bounds.width)),
)?;
drop(_g_viewbox);
let _g_finalize = timing.section(&mut timings.finalize_svg);
out = out.replacen(TITLE_PLACEHOLDER_COMMENT, title_svg.as_str(), 1);
drop(_g_finalize);
timings.total = total_timer
.map(merman_core::runtime::OperationTimer::elapsed)
.unwrap_or_default();
if timing.is_enabled() {
eprintln!(
"[render-timing] diagram=stateDiagram total={:?} deserialize={:?} build_ctx={:?} render_svg={:?} viewbox={:?} viewbox_svg_scan={:?} finalize={:?} root_calls={} clusters={:?} edge_paths={:?} edge_labels={:?} leaf_nodes={:?} leaf_style_parse={:?} leaf_roughjs={:?} leaf_roughjs_calls={} leaf_roughjs_unique={} leaf_measure={:?} leaf_label_html={:?} leaf_emit={:?} nested_roots={:?} self_loop_placeholders={:?}",
timings.total,
timings.deserialize_model,
timings.build_ctx,
timings.render_svg,
timings.viewbox,
viewbox_svg_scan,
timings.finalize_svg,
detail.root_calls,
detail.clusters,
detail.edge_paths,
detail.edge_labels,
detail.leaf_nodes,
detail.leaf_nodes_style_parse,
detail.leaf_nodes_roughjs,
detail.leaf_roughjs_calls,
detail.leaf_roughjs_unique.len(),
detail.leaf_nodes_measure,
detail.leaf_nodes_label_html,
detail.leaf_nodes_emit,
detail.nested_roots,
detail.self_loop_placeholders,
);
}
root_document.complete(out)
}
fn render_state_root(
out: &mut String,
ctx: &StateRenderCtx<'_>,
root: Option<&str>,
parent_origin: (f64, f64),
options: &SvgExecution<'_>,
timing: super::timing::RenderTiming,
details: &mut StateRenderDetails,
) -> Result<()> {
details.root_calls += 1;
options.checkpoint_emit()?;
let (parent_origin_x, parent_origin_y) = parent_origin;
const GRAPH_MARGIN_PX: f64 = 8.0;
let (origin_x, origin_y, transform_attr) = if let Some(root_id) = root {
if let Some(c) = ctx.layout_clusters_by_id.get(root_id).copied() {
let left = c.x - c.width / 2.0;
let top = c.y - c.height / 2.0;
let origin_x = left - GRAPH_MARGIN_PX;
let origin_y = top - GRAPH_MARGIN_PX;
let tx = origin_x - parent_origin_x;
let ty = origin_y - parent_origin_y;
(
origin_x,
origin_y,
format!(r#" transform="translate({}, {})""#, fmt(tx), fmt(ty)),
)
} else {
(
parent_origin_x,
parent_origin_y,
r#" transform="translate(0, 0)""#.to_string(),
)
}
} else {
(parent_origin_x, parent_origin_y, String::new())
};
let _ = write!(out, r#"<g class="root"{}>"#, transform_attr);
let _g_clusters = detail_guard(timing, &mut details.clusters);
out.push_str(r#"<g class="clusters">"#);
if let Some(root_id) = root {
render_state_cluster(out, ctx, root_id, origin_x, origin_y);
options.checkpoint_emit()?;
}
for &cluster_id in &ctx.node_order {
if root == Some(cluster_id) {
continue;
}
if !ctx.layout_clusters_by_id.contains_key(cluster_id) {
continue;
}
if state_is_hidden(ctx, cluster_id) {
continue;
}
if ctx.nested_roots.contains(cluster_id) {
continue;
}
let Some(node) = ctx.nodes_by_id.get(cluster_id).copied() else {
continue;
};
if !node.is_group || node.shape == "noteGroup" {
continue;
}
if state_insertion_context(ctx, cluster_id) != root {
continue;
}
render_state_cluster(out, ctx, cluster_id, origin_x, origin_y);
options.checkpoint_emit()?;
}
for &cluster_id in &ctx.node_order {
if !ctx.layout_clusters_by_id.contains_key(cluster_id) {
continue;
}
let Some(cluster) = ctx.layout_clusters_by_id.get(cluster_id).copied() else {
continue;
};
if state_is_hidden(ctx, cluster_id) {
continue;
}
let Some(node) = ctx.nodes_by_id.get(cluster_id).copied() else {
continue;
};
if node.shape != "noteGroup" {
continue;
}
let note_owner = state_note_owner_id(cluster_id);
if ctx.hidden_prefixes.iter().any(|p| p == note_owner) {
continue;
}
let has_position = ctx
.states
.get(note_owner)
.and_then(|s| s.note.as_ref())
.and_then(|n| n.position.as_ref())
.is_some();
if !has_position {
continue;
}
let target_root = state_insertion_context(ctx, note_owner);
if target_root != root {
continue;
}
let left = cluster.x - cluster.width / 2.0;
let top = cluster.y - cluster.height / 2.0;
let x = left - origin_x;
let y = top - origin_y;
let dom_id = state_node_scoped_dom_id(ctx, cluster_id);
let _ = write!(
out,
r#"<g id="{}" class="note-cluster"><rect x="{}" y="{}" width="{}" height="{}" fill="none"/></g>"#,
dom_id,
fmt_display(x),
fmt_display(y),
fmt_display(cluster.width.max(1.0)),
fmt_display(cluster.height.max(1.0))
);
options.checkpoint_emit()?;
}
out.push_str("</g>");
drop(_g_clusters);
let _g_edge_paths = detail_guard(timing, &mut details.edge_paths);
out.push_str(r#"<g class="edgePaths">"#);
if ctx.include_edges {
for (edge_index, edge) in ctx.edges.iter().enumerate() {
if state_is_hidden(ctx, edge.start.as_str())
|| state_is_hidden(ctx, edge.end.as_str())
|| state_is_hidden(ctx, edge.id.as_str())
{
continue;
}
if state_edge_context(ctx, edge) != root {
continue;
}
if state_is_shadowed_self_loop_edge(ctx, edge_index, edge, root) {
continue;
}
render_state_edge_path(out, ctx, edge, origin_x, origin_y);
options.checkpoint_emit()?;
}
}
out.push_str("</g>");
drop(_g_edge_paths);
let _g_edge_labels = detail_guard(timing, &mut details.edge_labels);
out.push_str(r#"<g class="edgeLabels">"#);
if ctx.include_edges {
for (edge_index, edge) in ctx.edges.iter().enumerate() {
if state_is_hidden(ctx, edge.start.as_str())
|| state_is_hidden(ctx, edge.end.as_str())
|| state_is_hidden(ctx, edge.id.as_str())
{
continue;
}
if state_edge_context(ctx, edge) != root {
continue;
}
if state_is_shadowed_self_loop_edge(ctx, edge_index, edge, root) {
continue;
}
render_state_edge_label(out, ctx, edge, origin_x, origin_y);
options.checkpoint_emit()?;
}
}
out.push_str("</g>");
drop(_g_edge_labels);
out.push_str(r#"<g class="nodes">"#);
let mut nested: Vec<&str> = Vec::new();
for &id in &ctx.node_order {
let Some(n) = ctx.nodes_by_id.get(id).copied() else {
continue;
};
if state_is_hidden(ctx, id) {
continue;
}
if n.is_group
&& n.shape != "noteGroup"
&& ctx.nested_roots.contains(id)
&& state_insertion_context(ctx, id) == root
{
nested.push(id);
}
}
if ctx.include_nodes {
let leaf_start = timing.start();
for &id in &ctx.node_order {
let Some(n) = ctx.layout_nodes_by_id.get(id).copied() else {
continue;
};
if state_is_hidden(ctx, id) {
continue;
}
if n.is_cluster {
continue;
}
if state_leaf_context(ctx, id) != root {
continue;
}
render_state_node_svg(out, ctx, id, origin_x, origin_y, timing, details);
options.checkpoint_emit()?;
}
if let Some(s) = leaf_start {
details.leaf_nodes += s.elapsed();
}
}
for child_root in nested {
let nested_start = timing.start();
render_state_root(
out,
ctx,
Some(child_root),
(origin_x, origin_y),
options,
timing,
details,
)?;
options.checkpoint_emit()?;
if let Some(s) = nested_start {
details.nested_roots += s.elapsed();
}
}
if ctx.include_edges {
let _g_placeholders = detail_guard(timing, &mut details.self_loop_placeholders);
for (edge_index, edge) in ctx.edges.iter().enumerate() {
if state_is_hidden(ctx, edge.start.as_str())
|| state_is_hidden(ctx, edge.end.as_str())
|| state_is_hidden(ctx, edge.id.as_str())
{
continue;
}
if edge.start != edge.end {
continue;
}
if state_edge_context(ctx, edge) != root {
continue;
}
if state_is_shadowed_self_loop_edge(ctx, edge_index, edge, root) {
continue;
}
let start = edge.start.as_str();
let id1 = format!("{start}---{start}---1");
let id2 = format!("{start}---{start}---2");
for id in [id1, id2] {
let (cx, cy) = ctx
.layout_nodes_by_id
.get(id.as_str())
.map(|n| {
let x = (n.x - n.width / 2.0) - origin_x;
let y = (n.y - n.height / 2.0) - origin_y;
(x, y)
})
.unwrap_or((0.0, 0.0));
if ctx.html_labels {
let _ = write!(
out,
r#"<g class="label edgeLabel" id="{}" transform="translate({}, {})"><rect width="0.1" height="0.1"/><g class="label" style="" transform="translate(0, 0)"><rect/><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 10px; text-align: center;"><span class="nodeLabel"></span></div></foreignObject></g></g>"#,
escape_xml_display(&id),
fmt_display(cx),
fmt_display(cy),
);
} else {
let _ = write!(
out,
r#"<g class="label edgeLabel" id="{}" transform="translate({}, {})"><rect width="0.1" height="0.1"/><g class="label" style="" transform="translate(0, 0)"><rect/></g></g>"#,
escape_xml_display(&id),
fmt_display(cx),
fmt_display(cy),
);
}
options.checkpoint_emit()?;
}
}
drop(_g_placeholders);
}
out.push_str("</g>");
out.push_str("</g>");
Ok(())
}
fn render_state_cluster(
out: &mut String,
ctx: &StateRenderCtx<'_>,
cluster_id: &str,
origin_x: f64,
origin_y: f64,
) {
let Some(cluster) = ctx.layout_clusters_by_id.get(cluster_id).copied() else {
return;
};
let data_look = state_data_look(ctx);
let shape = ctx
.nodes_by_id
.get(cluster_id)
.copied()
.map(|n| n.shape.as_str())
.unwrap_or("");
let class = ctx
.nodes_by_id
.get(cluster_id)
.copied()
.map(|n| n.css_classes.trim())
.filter(|c| !c.is_empty())
.unwrap_or("statediagram-state statediagram-cluster");
let left = cluster.x - cluster.width / 2.0;
let top = cluster.y - cluster.height / 2.0;
let x = left - origin_x;
let y = top - origin_y;
let dom_id = state_node_scoped_dom_id(ctx, cluster_id);
if shape == "divider" {
let _ = write!(
out,
r#"<g class="{}" id="{}" data-look="{}"><g><rect class="divider" x="{}" y="{}" width="{}" height="{}" data-look="{}"/></g></g>"#,
escape_attr(class),
dom_id.attr(),
escape_attr(data_look),
fmt(x),
fmt(y),
fmt(cluster.width.max(1.0)),
fmt(cluster.height.max(1.0)),
escape_attr(data_look),
);
return;
}
let title = ctx
.nodes_by_id
.get(cluster_id)
.copied()
.map(state_node_label_text)
.unwrap_or_else(|| cluster_id.to_string());
if ctx.html_labels {
let _ = write!(
out,
r#"<g class="{}" id="{}" data-id="{}" data-look="{}"><g><rect class="outer" x="{}" y="{}" width="{}" height="{}" data-look="{}"/></g><g class="cluster-label" transform="translate({}, {})"><foreignObject width="{}" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>{}</p></span></div></foreignObject></g><rect class="inner" x="{}" y="{}" width="{}" height="{}"/></g>"#,
escape_attr(class),
dom_id.attr(),
escape_attr(cluster_id),
escape_attr(data_look),
fmt(x),
fmt(y),
fmt(cluster.width.max(1.0)),
fmt(cluster.height.max(1.0)),
escape_attr(data_look),
fmt(x + (cluster.width.max(1.0) - cluster.title_label.width.max(0.0)) / 2.0),
fmt(y + 1.0),
fmt(cluster.title_label.width.max(0.0)),
escape_xml(&title),
fmt(x),
fmt(y + 26.0),
fmt(cluster.width.max(1.0)),
fmt((cluster.height - 30.0).max(1.0))
);
} else {
let title_dom = state_svg_text_label(&title, false, None);
let _ = write!(
out,
r#"<g class="{}" id="{}" data-id="{}" data-look="{}"><g><rect class="outer" x="{}" y="{}" width="{}" height="{}" data-look="{}"/></g><g class="cluster-label" transform="translate({}, {})">{}</g><rect class="inner" x="{}" y="{}" width="{}" height="{}"/></g>"#,
escape_attr(class),
dom_id.attr(),
escape_attr(cluster_id),
escape_attr(data_look),
fmt(x),
fmt(y),
fmt(cluster.width.max(1.0)),
fmt(cluster.height.max(1.0)),
escape_attr(data_look),
fmt(x + (cluster.width.max(1.0) - cluster.title_label.width.max(0.0)) / 2.0),
fmt(y + 1.0),
title_dom,
fmt(x),
fmt(y + 21.0),
fmt(cluster.width.max(1.0)),
fmt((cluster.height - 29.0).max(1.0))
);
}
}