use super::*;
pub(super) fn compute_architecture_layout(
graph: &Graph,
theme: &Theme,
config: &LayoutConfig,
) -> Layout {
const MARGIN: f32 = 24.0;
const SERVICE_SIZE: f32 = 64.0;
const JUNCTION_SIZE: f32 = 18.0;
const SERVICE_GAP: f32 = 72.0;
const GROUP_PAD_X: f32 = 28.0;
const GROUP_PAD_TOP: f32 = 32.0;
const GROUP_PAD_BOTTOM: f32 = 44.0;
const GROUP_GAP_Y: f32 = 48.0;
const GROUP_STROKE: &str = "hsl(240, 60%, 86.2745098039%)";
const ICON_FILL: &str = "#087ebf";
let mut nodes = BTreeMap::new();
for node in graph.nodes.values() {
let is_junction = node.icon.as_deref() == Some("junction")
|| (node.shape == crate::mermaid_engine::ir::NodeShape::Circle
&& node.label.trim().is_empty());
let label = measure_label(&node.label, theme, config);
let mut style = resolve_node_style(node.id.as_str(), graph);
if style.fill.is_none() {
style.fill = Some(if is_junction {
theme.line_color.clone()
} else {
ICON_FILL.to_string()
});
}
if style.stroke.is_none() {
style.stroke = Some("none".to_string());
}
if style.stroke_width.is_none() {
style.stroke_width = Some(0.0);
}
let size = if is_junction {
JUNCTION_SIZE
} else {
SERVICE_SIZE
};
let mut nl = build_node_layout(node, label, size, size, style, graph);
nl.shape = if is_junction {
crate::mermaid_engine::ir::NodeShape::Circle
} else {
crate::mermaid_engine::ir::NodeShape::Rectangle
};
nl.icon = node.icon.clone();
nodes.insert(node.id.clone(), nl);
}
let mut assigned: HashSet<String> = HashSet::new();
let mut subgraphs = Vec::new();
let mut current_y = MARGIN;
for sub in &graph.subgraphs {
let mut group_nodes: Vec<String> = sub
.nodes
.iter()
.filter(|id| nodes.contains_key(*id))
.cloned()
.collect();
if group_nodes.is_empty() {
continue;
}
group_nodes.sort_by(|a, b| {
let order_a = graph.node_order.get(a).copied().unwrap_or(usize::MAX);
let order_b = graph.node_order.get(b).copied().unwrap_or(usize::MAX);
order_a.cmp(&order_b).then_with(|| a.cmp(b))
});
assigned.extend(group_nodes.iter().cloned());
let count = group_nodes.len() as f32;
let gaps = (count - 1.0).max(0.0);
let group_width = GROUP_PAD_X * 2.0 + SERVICE_SIZE * count + SERVICE_GAP * gaps;
let group_height = GROUP_PAD_TOP + SERVICE_SIZE + GROUP_PAD_BOTTOM;
let group_x = MARGIN;
let group_y = current_y;
let mut x_cursor = group_x + GROUP_PAD_X;
for node_id in &group_nodes {
if let Some(node) = nodes.get_mut(node_id) {
node.x = x_cursor;
node.y = group_y + GROUP_PAD_TOP;
}
x_cursor += SERVICE_SIZE + SERVICE_GAP;
}
let label_block = measure_label(&sub.label, theme, config);
let mut style = resolve_subgraph_style(sub, graph);
style.fill = Some("none".to_string());
style.stroke = Some(GROUP_STROKE.to_string());
style.stroke_width = Some(2.0);
style.stroke_dasharray = Some("8".to_string());
if style.text_color.is_none() {
style.text_color = Some(theme.primary_text_color.clone());
}
subgraphs.push(SubgraphLayout {
label: sub.label.clone(),
label_block,
nodes: group_nodes,
x: group_x,
y: group_y,
width: group_width,
height: group_height,
style,
icon: sub.icon.clone(),
});
current_y += group_height + GROUP_GAP_Y;
}
let mut free_nodes: Vec<String> = nodes
.keys()
.filter(|id| !assigned.contains(*id))
.cloned()
.collect();
free_nodes.sort_by(|a, b| {
let order_a = graph.node_order.get(a).copied().unwrap_or(usize::MAX);
let order_b = graph.node_order.get(b).copied().unwrap_or(usize::MAX);
order_a.cmp(&order_b).then_with(|| a.cmp(b))
});
if !free_nodes.is_empty() {
let row_y = current_y;
let mut x_cursor = MARGIN + GROUP_PAD_X;
for node_id in &free_nodes {
if let Some(node) = nodes.get_mut(node_id) {
node.x = x_cursor;
node.y = row_y + GROUP_PAD_TOP;
}
x_cursor += SERVICE_SIZE + SERVICE_GAP;
}
}
let mut edges = Vec::new();
for (idx, edge) in graph.edges.iter().enumerate() {
let Some(from) = nodes.get(&edge.from) else {
continue;
};
let Some(to) = nodes.get(&edge.to) else {
continue;
};
let (start_side, end_side, _is_backward) = edge_sides(from, to, graph.direction);
let start = anchor_point_for_node(from, start_side, 0.0);
let end = anchor_point_for_node(to, end_side, 0.0);
let mut points = vec![start];
let dx = (start.0 - end.0).abs();
let dy = (start.1 - end.1).abs();
if dx > 1e-3 && dy <= 1e-3 {
let y = start.1;
let seg_min_x = start.0.min(end.0);
let seg_max_x = start.0.max(end.0);
let mut block_top = f32::MAX;
let mut block_bottom = f32::MIN;
let mut has_blocker = false;
for node in nodes.values() {
if node.id == edge.from || node.id == edge.to {
continue;
}
let node_min_x = node.x;
let node_max_x = node.x + node.width;
let node_min_y = node.y;
let node_max_y = node.y + node.height;
if y > node_min_y
&& y < node_max_y
&& seg_max_x > node_min_x
&& seg_min_x < node_max_x
{
has_blocker = true;
block_top = block_top.min(node_min_y);
block_bottom = block_bottom.max(node_max_y);
}
}
if has_blocker {
let gap = 16.0;
let above = block_top - gap;
let below = block_bottom + gap;
let detour_y = if (y - above).abs() <= (below - y).abs() {
above
} else {
below
};
points.push((start.0, detour_y));
points.push((end.0, detour_y));
}
} else if dx > 1e-3 && dy > 1e-3 {
if side_is_vertical(start_side) {
points.push((start.0, end.1));
} else {
points.push((end.0, start.1));
}
}
points.push(end);
let mut override_style = resolve_edge_style(idx, graph);
if override_style.stroke.is_none() {
override_style.stroke = Some(theme.line_color.clone());
}
override_style.stroke_width = Some(override_style.stroke_width.unwrap_or(3.0));
edges.push(EdgeLayout {
from: edge.from.clone(),
to: edge.to.clone(),
label: None,
start_label: None,
end_label: None,
label_anchor: None,
start_label_anchor: None,
end_label_anchor: None,
points: compress_path(&points),
directed: edge.directed,
arrow_start: edge.arrow_start,
arrow_end: edge.arrow_end,
arrow_start_kind: None,
arrow_end_kind: None,
start_decoration: None,
end_decoration: None,
style: edge.style,
override_style,
});
}
let (max_x, max_y) = bounds_with_edges(&nodes, &subgraphs, &edges);
let width = (max_x + MARGIN).max(200.0);
let height = (max_y + MARGIN).max(200.0);
Layout {
kind: graph.kind,
nodes,
edges,
subgraphs,
width,
height,
diagram: DiagramData::Graph {
state_notes: Vec::new(),
},
}
}