1use crate::math::MathRenderer;
2use crate::model::{
3 FlowchartV2Layout, LayoutCluster, LayoutEdge, LayoutLabel, LayoutNode, LayoutPoint,
4};
5use crate::text::{TextMeasurer, TextStyle, WrapMode};
6use crate::{Error, Result};
7use dugong::graphlib::{Graph, GraphOptions};
8use dugong::{EdgeLabel, GraphLabel, LabelPos, NodeLabel, RankDir};
9use merman_core::MermaidConfig;
10use serde_json::Value;
11use std::collections::HashMap;
12
13use super::label::compute_bounds;
14use super::node::node_layout_dimensions;
15use super::{FlowEdge, FlowSubgraph, FlowchartV2Model};
16use super::{flowchart_effective_text_style_for_classes, flowchart_label_metrics_for_layout};
17
18fn json_f64(v: &Value) -> Option<f64> {
19 v.as_f64()
20 .or_else(|| v.as_i64().map(|n| n as f64))
21 .or_else(|| v.as_u64().map(|n| n as f64))
22}
23
24fn config_f64(cfg: &Value, path: &[&str]) -> Option<f64> {
25 let mut cur = cfg;
26 for key in path {
27 cur = cur.get(*key)?;
28 }
29 json_f64(cur)
30}
31
32fn config_string(cfg: &Value, path: &[&str]) -> Option<String> {
33 let mut cur = cfg;
34 for key in path {
35 cur = cur.get(*key)?;
36 }
37 cur.as_str().map(|s| s.to_string())
38}
39
40fn rank_dir_from_flow(direction: &str) -> RankDir {
41 match direction.trim().to_uppercase().as_str() {
42 "TB" | "TD" => RankDir::TB,
43 "BT" => RankDir::BT,
44 "LR" => RankDir::LR,
45 "RL" => RankDir::RL,
46 _ => RankDir::TB,
47 }
48}
49
50fn normalize_dir(s: &str) -> String {
51 s.trim().to_uppercase()
52}
53
54fn toggled_dir(parent: &str) -> String {
55 let parent = normalize_dir(parent);
56 if parent == "TB" || parent == "TD" {
57 "LR".to_string()
58 } else {
59 "TB".to_string()
60 }
61}
62
63fn flow_dir_from_rankdir(rankdir: RankDir) -> &'static str {
64 match rankdir {
65 RankDir::TB => "TB",
66 RankDir::BT => "BT",
67 RankDir::LR => "LR",
68 RankDir::RL => "RL",
69 }
70}
71
72fn effective_cluster_dir(sg: &FlowSubgraph, parent_dir: &str, inherit_dir: bool) -> String {
73 if let Some(dir) = sg.dir.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
74 return normalize_dir(dir);
75 }
76 if inherit_dir {
77 return normalize_dir(parent_dir);
78 }
79 toggled_dir(parent_dir)
80}
81
82fn compute_effective_dir_by_id(
83 subgraphs_by_id: &HashMap<String, FlowSubgraph>,
84 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
85 diagram_dir: &str,
86 inherit_dir: bool,
87) -> HashMap<String, String> {
88 fn compute_one(
89 id: &str,
90 subgraphs_by_id: &HashMap<String, FlowSubgraph>,
91 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
92 diagram_dir: &str,
93 inherit_dir: bool,
94 visiting: &mut std::collections::HashSet<String>,
95 memo: &mut HashMap<String, String>,
96 ) -> String {
97 if let Some(dir) = memo.get(id) {
98 return dir.clone();
99 }
100 if !visiting.insert(id.to_string()) {
101 let dir = toggled_dir(diagram_dir);
102 memo.insert(id.to_string(), dir.clone());
103 return dir;
104 }
105
106 let parent_dir = g
107 .parent(id)
108 .and_then(|p| subgraphs_by_id.contains_key(p).then_some(p.to_string()))
109 .map(|p| {
110 compute_one(
111 &p,
112 subgraphs_by_id,
113 g,
114 diagram_dir,
115 inherit_dir,
116 visiting,
117 memo,
118 )
119 })
120 .unwrap_or_else(|| normalize_dir(diagram_dir));
121
122 let dir = subgraphs_by_id
123 .get(id)
124 .map(|sg| effective_cluster_dir(sg, &parent_dir, inherit_dir))
125 .unwrap_or_else(|| toggled_dir(&parent_dir));
126
127 memo.insert(id.to_string(), dir.clone());
128 let _ = visiting.remove(id);
129 dir
130 }
131
132 let mut memo: HashMap<String, String> = HashMap::new();
133 let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
134 for id in subgraphs_by_id.keys() {
135 let _ = compute_one(
136 id,
137 subgraphs_by_id,
138 g,
139 diagram_dir,
140 inherit_dir,
141 &mut visiting,
142 &mut memo,
143 );
144 }
145 memo
146}
147
148fn dir_to_rankdir(dir: &str) -> RankDir {
149 match normalize_dir(dir).as_str() {
150 "TB" | "TD" => RankDir::TB,
151 "BT" => RankDir::BT,
152 "LR" => RankDir::LR,
153 "RL" => RankDir::RL,
154 _ => RankDir::TB,
155 }
156}
157
158fn edge_label_is_non_empty(edge: &FlowEdge) -> bool {
159 edge.label
160 .as_deref()
161 .map(|s| !s.trim().is_empty())
162 .unwrap_or(false)
163}
164
165#[cfg(feature = "flowchart_root_pack")]
166fn edge_label_leaf_id(edge: &FlowEdge) -> String {
167 format!("edge-label::{}", edge.id)
168}
169
170fn lowest_common_parent(
171 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
172 a: &str,
173 b: &str,
174) -> Option<String> {
175 if !g.options().compound {
176 return None;
177 }
178
179 let mut ancestors: std::collections::HashSet<String> = std::collections::HashSet::new();
180 let mut cur = g.parent(a);
181 while let Some(p) = cur {
182 ancestors.insert(p.to_string());
183 cur = g.parent(p);
184 }
185
186 let mut cur = g.parent(b);
187 while let Some(p) = cur {
188 if ancestors.contains(p) {
189 return Some(p.to_string());
190 }
191 cur = g.parent(p);
192 }
193
194 None
195}
196
197fn extract_descendants(id: &str, g: &Graph<NodeLabel, EdgeLabel, GraphLabel>) -> Vec<String> {
198 let children = g.children(id);
199 let mut out: Vec<String> = children.iter().map(|s| s.to_string()).collect();
200 for child in children {
201 out.extend(extract_descendants(child, g));
202 }
203 out
204}
205
206fn edge_in_cluster(
207 edge: &dugong::graphlib::EdgeKey,
208 cluster_id: &str,
209 descendants: &std::collections::HashMap<String, Vec<String>>,
210) -> bool {
211 if edge.v == cluster_id || edge.w == cluster_id {
212 return false;
213 }
214 let Some(cluster_desc) = descendants.get(cluster_id) else {
215 return false;
216 };
217 cluster_desc.contains(&edge.v) || cluster_desc.contains(&edge.w)
218}
219
220#[derive(Debug, Clone)]
221struct FlowchartClusterDbEntry {
222 anchor_id: String,
223 external_connections: bool,
224}
225
226fn flowchart_find_common_edges(
227 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
228 id1: &str,
229 id2: &str,
230) -> Vec<(String, String)> {
231 let edges1: Vec<(String, String)> = graph
232 .edge_keys()
233 .into_iter()
234 .filter(|edge| edge.v == id1 || edge.w == id1)
235 .map(|edge| (edge.v, edge.w))
236 .collect();
237 let edges2: Vec<(String, String)> = graph
238 .edge_keys()
239 .into_iter()
240 .filter(|edge| edge.v == id2 || edge.w == id2)
241 .map(|edge| (edge.v, edge.w))
242 .collect();
243
244 let edges1_prim: Vec<(String, String)> = edges1
245 .into_iter()
246 .map(|(v, w)| {
247 (
248 if v == id1 { id2.to_string() } else { v },
249 if w == id1 { id1.to_string() } else { w },
252 )
253 })
254 .collect();
255
256 let mut out = Vec::new();
257 for e1 in edges1_prim {
258 if edges2.contains(&e1) {
259 out.push(e1);
260 }
261 }
262 out
263}
264
265fn flowchart_find_non_cluster_child(
266 id: &str,
267 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
268 cluster_id: &str,
269) -> Option<String> {
270 let children = graph.children(id);
271 if children.is_empty() {
272 return Some(id.to_string());
273 }
274 let mut reserve: Option<String> = None;
275 for child in children {
276 let Some(child_id) = flowchart_find_non_cluster_child(child, graph, cluster_id) else {
277 continue;
278 };
279 let common_edges = flowchart_find_common_edges(graph, cluster_id, &child_id);
280 if !common_edges.is_empty() {
281 reserve = Some(child_id);
282 } else {
283 return Some(child_id);
284 }
285 }
286 reserve
287}
288
289fn adjust_flowchart_clusters_and_edges(graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>) {
290 use serde_json::Value;
291
292 fn is_descendant(
293 node_id: &str,
294 cluster_id: &str,
295 descendants: &std::collections::HashMap<String, Vec<String>>,
296 ) -> bool {
297 descendants
298 .get(cluster_id)
299 .is_some_and(|ds| ds.iter().any(|n| n == node_id))
300 }
301
302 let mut descendants: std::collections::HashMap<String, Vec<String>> =
303 std::collections::HashMap::new();
304 let mut cluster_db: std::collections::HashMap<String, FlowchartClusterDbEntry> =
305 std::collections::HashMap::new();
306
307 for id in graph.node_ids() {
308 if graph.children(&id).is_empty() {
309 continue;
310 }
311 descendants.insert(id.clone(), extract_descendants(&id, graph));
312 let anchor_id =
313 flowchart_find_non_cluster_child(&id, graph, &id).unwrap_or_else(|| id.clone());
314 cluster_db.insert(
315 id,
316 FlowchartClusterDbEntry {
317 anchor_id,
318 external_connections: false,
319 },
320 );
321 }
322
323 for id in cluster_db.keys().cloned().collect::<Vec<_>>() {
324 if graph.children(&id).is_empty() {
325 continue;
326 }
327 let mut has_external = false;
328 for e in graph.edges() {
329 let d1 = is_descendant(e.v.as_str(), id.as_str(), &descendants);
330 let d2 = is_descendant(e.w.as_str(), id.as_str(), &descendants);
331 if d1 ^ d2 {
332 has_external = true;
333 break;
334 }
335 }
336 if let Some(entry) = cluster_db.get_mut(&id) {
337 entry.external_connections = has_external;
338 }
339 }
340
341 for id in cluster_db.keys().cloned().collect::<Vec<_>>() {
342 let Some(non_cluster_child) = cluster_db.get(&id).map(|e| e.anchor_id.clone()) else {
343 continue;
344 };
345 let parent = graph.parent(&non_cluster_child);
346 if parent.is_some_and(|p| p != id.as_str())
347 && parent.is_some_and(|p| cluster_db.contains_key(p))
348 && parent.is_some_and(|p| !cluster_db.get(p).is_some_and(|e| e.external_connections))
349 {
350 if let Some(p) = parent {
351 if let Some(entry) = cluster_db.get_mut(&id) {
352 entry.anchor_id = p.to_string();
353 }
354 }
355 }
356 }
357
358 fn get_anchor_id(
359 id: &str,
360 cluster_db: &std::collections::HashMap<String, FlowchartClusterDbEntry>,
361 ) -> String {
362 let Some(entry) = cluster_db.get(id) else {
363 return id.to_string();
364 };
365 if !entry.external_connections {
366 return id.to_string();
367 }
368 entry.anchor_id.clone()
369 }
370
371 let edge_keys = graph.edge_keys();
372 for ek in edge_keys {
373 if !cluster_db.contains_key(&ek.v) && !cluster_db.contains_key(&ek.w) {
374 continue;
375 }
376
377 let Some(mut edge_label) = graph.edge_by_key(&ek).cloned() else {
378 continue;
379 };
380
381 let v = get_anchor_id(&ek.v, &cluster_db);
382 let w = get_anchor_id(&ek.w, &cluster_db);
383
384 let _ = graph.remove_edge_key(&ek);
388
389 if v != ek.v {
390 if let Some(parent) = graph.parent(&v) {
391 if let Some(entry) = cluster_db.get_mut(parent) {
392 entry.external_connections = true;
393 }
394 }
395 edge_label
396 .extras
397 .insert("fromCluster".to_string(), Value::String(ek.v.clone()));
398 }
399
400 if w != ek.w {
401 if let Some(parent) = graph.parent(&w) {
402 if let Some(entry) = cluster_db.get_mut(parent) {
403 entry.external_connections = true;
404 }
405 }
406 edge_label
407 .extras
408 .insert("toCluster".to_string(), Value::String(ek.w.clone()));
409 }
410
411 graph.set_edge_named(v, w, ek.name, Some(edge_label));
412 }
413}
414
415fn copy_cluster(
416 cluster_id: &str,
417 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
418 new_graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
419 root_id: &str,
420 descendants: &std::collections::HashMap<String, Vec<String>>,
421) {
422 let mut nodes: Vec<String> = graph
423 .children(cluster_id)
424 .iter()
425 .map(|s| s.to_string())
426 .collect();
427 if cluster_id != root_id {
428 nodes.push(cluster_id.to_string());
429 }
430
431 for node in nodes {
432 if !graph.has_node(&node) {
433 continue;
434 }
435
436 if !graph.children(&node).is_empty() {
437 copy_cluster(&node, graph, new_graph, root_id, descendants);
438 } else {
439 let data = graph.node(&node).cloned().unwrap_or_default();
440 new_graph.set_node(node.clone(), data);
441
442 if let Some(parent) = graph.parent(&node) {
443 if parent != root_id {
444 new_graph.set_parent(node.clone(), parent.to_string());
445 }
446 }
447 if cluster_id != root_id && node != cluster_id {
448 new_graph.set_parent(node.clone(), cluster_id.to_string());
449 }
450
451 for ek in graph.edge_keys() {
462 if !edge_in_cluster(&ek, root_id, descendants) {
463 continue;
464 }
465 if new_graph.has_edge(&ek.v, &ek.w, ek.name.as_deref()) {
466 continue;
467 }
468 let Some(lbl) = graph.edge_by_key(&ek).cloned() else {
469 continue;
470 };
471 new_graph.set_edge_named(ek.v, ek.w, ek.name, Some(lbl));
472 }
473 }
474
475 let _ = graph.remove_node(&node);
476 }
477}
478
479fn extract_clusters_recursively(
480 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
481 subgraphs_by_id: &std::collections::HashMap<String, FlowSubgraph>,
482 _effective_dir_by_id: &std::collections::HashMap<String, String>,
483 extracted: &mut std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
484 depth: usize,
485) {
486 if depth > 10 {
487 return;
488 }
489
490 let node_ids = graph.node_ids();
491 let mut descendants: std::collections::HashMap<String, Vec<String>> =
492 std::collections::HashMap::new();
493 for id in &node_ids {
494 if graph.children(id).is_empty() {
495 continue;
496 }
497 descendants.insert(id.clone(), extract_descendants(id, graph));
498 }
499
500 let mut external: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
501 for id in descendants.keys() {
502 let Some(ds) = descendants.get(id) else {
503 continue;
504 };
505 let mut has_external = false;
506 for e in graph.edges() {
507 let d1 = ds.contains(&e.v);
508 let d2 = ds.contains(&e.w);
509 if d1 ^ d2 {
510 has_external = true;
511 break;
512 }
513 }
514 external.insert(id.clone(), has_external);
515 }
516
517 let mut extracted_here: Vec<(String, Graph<NodeLabel, EdgeLabel, GraphLabel>)> = Vec::new();
518
519 let candidates: Vec<String> = node_ids
520 .into_iter()
521 .filter(|id| graph.has_node(id))
522 .filter(|id| !graph.children(id).is_empty())
523 .filter(|id| !external.get(id).copied().unwrap_or(false))
529 .collect();
530
531 for id in candidates {
532 if !graph.has_node(&id) {
533 continue;
534 }
535 if graph.children(&id).is_empty() {
536 continue;
537 }
538
539 let mut cluster_graph: Graph<NodeLabel, EdgeLabel, GraphLabel> = Graph::new(GraphOptions {
540 multigraph: true,
541 compound: true,
542 directed: true,
543 });
544
545 let dir = subgraphs_by_id
549 .get(&id)
550 .and_then(|sg| sg.dir.as_deref())
551 .map(str::trim)
552 .filter(|s| !s.is_empty())
553 .map(normalize_dir)
554 .unwrap_or_else(|| toggled_dir(flow_dir_from_rankdir(graph.graph().rankdir)));
555
556 cluster_graph.set_graph(GraphLabel {
557 rankdir: dir_to_rankdir(&dir),
558 nodesep: 50.0,
570 ranksep: 50.0,
571 marginx: 8.0,
572 marginy: 8.0,
573 acyclicer: None,
574 ..Default::default()
575 });
576
577 copy_cluster(&id, graph, &mut cluster_graph, &id, &descendants);
578 extracted_here.push((id, cluster_graph));
579 }
580
581 for (id, mut g) in extracted_here {
582 extract_clusters_recursively(
583 &mut g,
584 subgraphs_by_id,
585 _effective_dir_by_id,
586 extracted,
587 depth + 1,
588 );
589 extracted.insert(id, g);
590 }
591}
592
593pub fn layout_flowchart_v2(
594 semantic: &Value,
595 effective_config: &MermaidConfig,
596 measurer: &dyn TextMeasurer,
597 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
598) -> Result<FlowchartV2Layout> {
599 let timing_enabled = std::env::var("MERMAN_FLOWCHART_LAYOUT_TIMING")
600 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
601 .unwrap_or(false);
602 let total_start = timing_enabled.then(std::time::Instant::now);
603
604 let deserialize_start = timing_enabled.then(std::time::Instant::now);
605 let model: FlowchartV2Model = crate::json::from_value_ref(semantic)?;
606 let deserialize = deserialize_start.map(|s| s.elapsed()).unwrap_or_default();
607
608 layout_flowchart_v2_with_model(
609 &model,
610 effective_config,
611 measurer,
612 math_renderer,
613 timing_enabled,
614 total_start,
615 deserialize,
616 )
617}
618
619pub fn layout_flowchart_v2_typed(
620 model: &FlowchartV2Model,
621 effective_config: &MermaidConfig,
622 measurer: &dyn TextMeasurer,
623 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
624) -> Result<FlowchartV2Layout> {
625 let timing_enabled = std::env::var("MERMAN_FLOWCHART_LAYOUT_TIMING")
626 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
627 .unwrap_or(false);
628 let total_start = timing_enabled.then(std::time::Instant::now);
629
630 layout_flowchart_v2_with_model(
631 model,
632 effective_config,
633 measurer,
634 math_renderer,
635 timing_enabled,
636 total_start,
637 std::time::Duration::default(),
638 )
639}
640
641fn layout_flowchart_v2_with_model(
642 model: &FlowchartV2Model,
643 effective_config: &MermaidConfig,
644 measurer: &dyn TextMeasurer,
645 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
646 timing_enabled: bool,
647 total_start: Option<std::time::Instant>,
648 deserialize: std::time::Duration,
649) -> Result<FlowchartV2Layout> {
650 #[derive(Debug, Default, Clone)]
651 struct FlowchartLayoutTimings {
652 total: std::time::Duration,
653 deserialize: std::time::Duration,
654 expand_self_loops: std::time::Duration,
655 build_graph: std::time::Duration,
656 extract_clusters: std::time::Duration,
657 dom_order: std::time::Duration,
658 layout_recursive: std::time::Duration,
659 dagre_calls: u32,
660 dagre_total: std::time::Duration,
661 place_graph: std::time::Duration,
662 build_output: std::time::Duration,
663 }
664
665 let mut timings = FlowchartLayoutTimings::default();
666 timings.deserialize = deserialize;
667
668 let effective_config_value = effective_config.as_value();
669
670 let expand_self_loops_start = timing_enabled.then(std::time::Instant::now);
674 let mut render_edges: Vec<FlowEdge> = Vec::new();
675 let mut self_loop_label_node_ids: Vec<String> = Vec::new();
676 let mut self_loop_label_node_id_set: std::collections::HashSet<String> =
677 std::collections::HashSet::new();
678 for e in &model.edges {
679 if e.from != e.to {
680 render_edges.push(e.clone());
681 continue;
682 }
683
684 let node_id = e.from.clone();
685 let special_id_1 = format!("{node_id}---{node_id}---1");
686 let special_id_2 = format!("{node_id}---{node_id}---2");
687 if self_loop_label_node_id_set.insert(special_id_1.clone()) {
688 self_loop_label_node_ids.push(special_id_1.clone());
689 }
690 if self_loop_label_node_id_set.insert(special_id_2.clone()) {
691 self_loop_label_node_ids.push(special_id_2.clone());
692 }
693
694 let mut edge1 = e.clone();
695 edge1.id = format!("{node_id}-cyclic-special-1");
696 edge1.from = node_id.clone();
697 edge1.to = special_id_1.clone();
698 edge1.label = None;
699 edge1.label_type = None;
700 edge1.edge_type = Some("arrow_open".to_string());
701
702 let mut edge_mid = e.clone();
703 edge_mid.id = format!("{node_id}-cyclic-special-mid");
704 edge_mid.from = special_id_1.clone();
705 edge_mid.to = special_id_2.clone();
706 edge_mid.edge_type = Some("arrow_open".to_string());
707
708 let mut edge2 = e.clone();
709 edge2.id = format!("{node_id}-cyclic-special-2");
710 edge2.from = special_id_2.clone();
711 edge2.to = node_id.clone();
712 edge1.label = Some(String::new());
715 edge2.label = Some(String::new());
716
717 render_edges.push(edge1);
718 render_edges.push(edge_mid);
719 render_edges.push(edge2);
720 }
721 if let Some(s) = expand_self_loops_start {
722 timings.expand_self_loops = s.elapsed();
723 }
724
725 let build_graph_start = timing_enabled.then(std::time::Instant::now);
726
727 let nodesep = config_f64(effective_config_value, &["flowchart", "nodeSpacing"]).unwrap_or(50.0);
728 let ranksep = config_f64(effective_config_value, &["flowchart", "rankSpacing"]).unwrap_or(50.0);
729 let node_padding =
731 config_f64(effective_config_value, &["flowchart", "padding"]).unwrap_or(15.0);
732 let state_padding = config_f64(effective_config_value, &["state", "padding"]).unwrap_or(8.0);
735 let wrapping_width =
736 config_f64(effective_config_value, &["flowchart", "wrappingWidth"]).unwrap_or(200.0);
737 let cluster_title_wrapping_width = 200.0;
741 let node_html_labels = effective_config_value
745 .get("htmlLabels")
746 .and_then(Value::as_bool)
747 .unwrap_or(true);
748 let edge_html_labels = effective_config_value
749 .get("flowchart")
750 .and_then(|v| v.get("htmlLabels"))
751 .and_then(Value::as_bool)
752 .unwrap_or(node_html_labels);
753 let cluster_html_labels = edge_html_labels;
754 let node_wrap_mode = if node_html_labels {
755 WrapMode::HtmlLike
756 } else {
757 WrapMode::SvgLike
758 };
759 let cluster_wrap_mode = if cluster_html_labels {
760 WrapMode::HtmlLike
761 } else {
762 WrapMode::SvgLike
763 };
764 let edge_wrap_mode = if edge_html_labels {
765 WrapMode::HtmlLike
766 } else {
767 WrapMode::SvgLike
768 };
769 let cluster_padding = 8.0;
772 let title_margin_top = config_f64(
773 effective_config_value,
774 &["flowchart", "subGraphTitleMargin", "top"],
775 )
776 .unwrap_or(0.0);
777 let title_margin_bottom = config_f64(
778 effective_config_value,
779 &["flowchart", "subGraphTitleMargin", "bottom"],
780 )
781 .unwrap_or(0.0);
782 let title_total_margin = title_margin_top + title_margin_bottom;
783 let y_shift = title_total_margin / 2.0;
784 let inherit_dir = effective_config_value
785 .get("flowchart")
786 .and_then(|v| v.get("inheritDir"))
787 .and_then(Value::as_bool)
788 .unwrap_or(false);
789
790 let font_family = config_string(effective_config_value, &["fontFamily"])
791 .or_else(|| Some("\"trebuchet ms\", verdana, arial, sans-serif".to_string()));
792 let font_size = config_f64(effective_config_value, &["fontSize"]).unwrap_or(16.0);
793 let font_weight = config_string(effective_config_value, &["fontWeight"]);
794 let text_style = TextStyle {
795 font_family,
796 font_size,
797 font_weight,
798 };
799
800 let diagram_direction = normalize_dir(model.direction.as_deref().unwrap_or("TB"));
801 let has_subgraphs = !model.subgraphs.is_empty();
802 let mut subgraphs_by_id: std::collections::HashMap<String, FlowSubgraph> =
803 std::collections::HashMap::new();
804 for sg in &model.subgraphs {
805 subgraphs_by_id.insert(sg.id.clone(), sg.clone());
806 }
807 let subgraph_ids: std::collections::HashSet<&str> =
808 model.subgraphs.iter().map(|sg| sg.id.as_str()).collect();
809 let subgraph_id_set: std::collections::HashSet<String> =
810 model.subgraphs.iter().map(|sg| sg.id.clone()).collect();
811 let mut g: Graph<NodeLabel, EdgeLabel, GraphLabel> = Graph::new(GraphOptions {
812 multigraph: true,
813 compound: true,
816 directed: true,
817 });
818 g.set_graph(GraphLabel {
819 rankdir: rank_dir_from_flow(&diagram_direction),
820 nodesep,
821 ranksep,
822 marginx: 8.0,
823 marginy: 8.0,
824 acyclicer: None,
825 ..Default::default()
826 });
827
828 let mut empty_subgraph_ids: Vec<String> = Vec::new();
829 let mut cluster_node_labels: std::collections::HashMap<String, NodeLabel> =
830 std::collections::HashMap::new();
831 for sg in &model.subgraphs {
832 if sg.nodes.is_empty() {
833 empty_subgraph_ids.push(sg.id.clone());
836 continue;
837 }
838 cluster_node_labels.insert(sg.id.clone(), NodeLabel::default());
842 }
843
844 let mut leaf_node_labels: std::collections::HashMap<String, NodeLabel> =
845 std::collections::HashMap::new();
846 let mut leaf_label_metrics_by_id: HashMap<String, (f64, f64)> = HashMap::new();
847 leaf_label_metrics_by_id.reserve(model.nodes.len() + empty_subgraph_ids.len());
848 for n in &model.nodes {
849 if subgraph_ids.contains(n.id.as_str()) {
853 continue;
854 }
855 let raw_label = n.label.as_deref().unwrap_or(&n.id);
856 let label_type = n.label_type.as_deref().unwrap_or("text");
857 let node_text_style = flowchart_effective_text_style_for_classes(
858 &text_style,
859 &model.class_defs,
860 &n.classes,
861 &n.styles,
862 );
863 let mut metrics = flowchart_label_metrics_for_layout(
864 measurer,
865 raw_label,
866 label_type,
867 node_text_style.as_ref(),
868 Some(wrapping_width),
869 node_wrap_mode,
870 effective_config,
871 math_renderer,
872 );
873 let span_css_height_parity = n.classes.iter().any(|c| {
874 model.class_defs.get(c.as_str()).is_some_and(|styles| {
875 styles.iter().any(|s| {
876 matches!(
877 s.split_once(':').map(|p| p.0.trim()),
878 Some("background" | "border")
879 )
880 })
881 })
882 });
883 if span_css_height_parity {
884 crate::text::flowchart_apply_mermaid_styled_node_height_parity(
885 &mut metrics,
886 node_text_style.as_ref(),
887 );
888 }
889 leaf_label_metrics_by_id.insert(n.id.clone(), (metrics.width, metrics.height));
890 let (width, height) = node_layout_dimensions(
891 n.layout_shape.as_deref(),
892 metrics,
893 node_padding,
894 state_padding,
895 n.icon.as_deref(),
896 n.img.as_deref(),
897 n.pos.as_deref(),
898 n.asset_width,
899 n.asset_height,
900 );
901 leaf_node_labels.insert(
902 n.id.clone(),
903 NodeLabel {
904 width,
905 height,
906 ..Default::default()
907 },
908 );
909 }
910 for sg in &model.subgraphs {
911 if !sg.nodes.is_empty() {
912 continue;
913 }
914 let label_type = sg.label_type.as_deref().unwrap_or("text");
915 let sg_text_style = flowchart_effective_text_style_for_classes(
916 &text_style,
917 &model.class_defs,
918 &sg.classes,
919 &[],
920 );
921 let metrics = flowchart_label_metrics_for_layout(
922 measurer,
923 &sg.title,
924 label_type,
925 sg_text_style.as_ref(),
926 Some(cluster_title_wrapping_width),
927 node_wrap_mode,
928 effective_config,
929 math_renderer,
930 );
931 leaf_label_metrics_by_id.insert(sg.id.clone(), (metrics.width, metrics.height));
932 let (width, height) = node_layout_dimensions(
933 Some("squareRect"),
934 metrics,
935 cluster_padding,
936 state_padding,
937 None,
938 None,
939 None,
940 None,
941 None,
942 );
943 leaf_node_labels.insert(
944 sg.id.clone(),
945 NodeLabel {
946 width,
947 height,
948 ..Default::default()
949 },
950 );
951 }
952
953 let mut inserted: std::collections::HashSet<String> = std::collections::HashSet::new();
961 let mut parent_assigned: std::collections::HashSet<String> = std::collections::HashSet::new();
962 let insert_one = |id: &str,
963 g: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
964 inserted: &mut std::collections::HashSet<String>| {
965 if inserted.contains(id) {
966 return;
967 }
968 if let Some(lbl) = cluster_node_labels.get(id).cloned() {
969 g.set_node(id.to_string(), lbl);
970 inserted.insert(id.to_string());
971 return;
972 }
973 if let Some(lbl) = leaf_node_labels.get(id).cloned() {
974 g.set_node(id.to_string(), lbl);
975 inserted.insert(id.to_string());
976 }
977 };
978
979 if has_subgraphs {
980 let mut parent_by_id: std::collections::HashMap<String, String> =
983 std::collections::HashMap::new();
984 for sg in model.subgraphs.iter().rev() {
985 for child in &sg.nodes {
986 parent_by_id.insert(child.clone(), sg.id.clone());
987 }
988 }
989
990 let insert_with_parent =
991 |id: &str,
992 g: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
993 inserted: &mut std::collections::HashSet<String>,
994 parent_assigned: &mut std::collections::HashSet<String>| {
995 insert_one(id, g, inserted);
996 if !parent_assigned.insert(id.to_string()) {
997 return;
998 }
999 if let Some(p) = parent_by_id.get(id).cloned() {
1000 g.set_parent(id.to_string(), p);
1001 }
1002 };
1003
1004 for sg in model.subgraphs.iter().rev() {
1005 insert_with_parent(sg.id.as_str(), &mut g, &mut inserted, &mut parent_assigned);
1006 }
1007 for n in &model.nodes {
1008 insert_with_parent(n.id.as_str(), &mut g, &mut inserted, &mut parent_assigned);
1009 }
1010 for id in &model.vertex_calls {
1011 insert_with_parent(id.as_str(), &mut g, &mut inserted, &mut parent_assigned);
1012 }
1013 } else {
1014 for n in &model.nodes {
1016 insert_one(n.id.as_str(), &mut g, &mut inserted);
1017 }
1018 for id in &model.vertex_calls {
1019 insert_one(id.as_str(), &mut g, &mut inserted);
1020 }
1021 }
1022
1023 for id in &self_loop_label_node_ids {
1026 if !g.has_node(id) {
1027 g.set_node(
1028 id.clone(),
1029 NodeLabel {
1030 width: 0.1_f32 as f64,
1035 height: 0.1_f32 as f64,
1036 ..Default::default()
1037 },
1038 );
1039 }
1040 let Some((base, _)) = id.split_once("---") else {
1041 continue;
1042 };
1043 if let Some(p) = g.parent(base) {
1044 g.set_parent(id.clone(), p.to_string());
1045 }
1046 }
1047
1048 let effective_dir_by_id = if has_subgraphs {
1049 compute_effective_dir_by_id(&subgraphs_by_id, &g, &diagram_direction, inherit_dir)
1050 } else {
1051 HashMap::new()
1052 };
1053
1054 let mut edge_key_by_id: HashMap<String, String> = HashMap::new();
1058 let mut edge_id_by_key: HashMap<String, String> = HashMap::new();
1059
1060 for e in &render_edges {
1061 let edge_key = if let Some(prefix) = e.id.strip_suffix("-cyclic-special-1") {
1064 format!("{prefix}-cyclic-special-0")
1065 } else if let Some(prefix) = e.id.strip_suffix("-cyclic-special-mid") {
1066 format!("{prefix}-cyclic-special-1")
1067 } else if let Some(prefix) = e.id.strip_suffix("-cyclic-special-2") {
1068 format!("{prefix}-cyc<lic-special-2")
1071 } else {
1072 e.id.clone()
1073 };
1074 edge_key_by_id.insert(e.id.clone(), edge_key.clone());
1075 edge_id_by_key.insert(edge_key.clone(), e.id.clone());
1076
1077 let from = e.from.clone();
1078 let to = e.to.clone();
1079
1080 if edge_label_is_non_empty(e) {
1081 let label_text = e.label.as_deref().unwrap_or_default();
1082 let label_type = e.label_type.as_deref().unwrap_or("text");
1083 let edge_text_style = flowchart_effective_text_style_for_classes(
1084 &text_style,
1085 &model.class_defs,
1086 &e.classes,
1087 &e.style,
1088 );
1089 let metrics = flowchart_label_metrics_for_layout(
1090 measurer,
1091 label_text,
1092 label_type,
1093 edge_text_style.as_ref(),
1094 Some(wrapping_width),
1095 edge_wrap_mode,
1096 effective_config,
1097 math_renderer,
1098 );
1099 let (label_width, label_height) = if edge_html_labels {
1100 (metrics.width.max(1.0), metrics.height.max(1.0))
1101 } else {
1102 (
1105 (metrics.width + 4.0).max(1.0),
1106 (metrics.height + 4.0).max(1.0),
1107 )
1108 };
1109
1110 let minlen = e.length.max(1);
1111 let el = EdgeLabel {
1112 width: label_width,
1113 height: label_height,
1114 labelpos: LabelPos::C,
1115 labeloffset: 10.0,
1117 minlen,
1118 weight: 1.0,
1119 ..Default::default()
1120 };
1121
1122 g.set_edge_named(from, to, Some(edge_key), Some(el));
1123 } else {
1124 let el = EdgeLabel {
1125 width: 0.0,
1126 height: 0.0,
1127 labelpos: LabelPos::C,
1128 labeloffset: 10.0,
1130 minlen: e.length.max(1),
1131 weight: 1.0,
1132 ..Default::default()
1133 };
1134 g.set_edge_named(from, to, Some(edge_key), Some(el));
1135 }
1136 }
1137
1138 if has_subgraphs {
1139 adjust_flowchart_clusters_and_edges(&mut g);
1140 }
1141
1142 let mut edge_endpoints_by_id: HashMap<String, (String, String)> = HashMap::new();
1143 for ek in g.edge_keys() {
1144 let Some(edge_key) = ek.name.as_deref() else {
1145 continue;
1146 };
1147 let edge_id = edge_id_by_key
1148 .get(edge_key)
1149 .cloned()
1150 .unwrap_or_else(|| edge_key.to_string());
1151 edge_endpoints_by_id.insert(edge_id, (ek.v.clone(), ek.w.clone()));
1152 }
1153
1154 if let Some(s) = build_graph_start {
1155 timings.build_graph = s.elapsed();
1156 }
1157
1158 let mut extracted_graphs: std::collections::HashMap<
1159 String,
1160 Graph<NodeLabel, EdgeLabel, GraphLabel>,
1161 > = std::collections::HashMap::new();
1162 if has_subgraphs {
1163 let extract_start = timing_enabled.then(std::time::Instant::now);
1164 extract_clusters_recursively(
1165 &mut g,
1166 &subgraphs_by_id,
1167 &effective_dir_by_id,
1168 &mut extracted_graphs,
1169 0,
1170 );
1171 if let Some(s) = extract_start {
1172 timings.extract_clusters = s.elapsed();
1173 }
1174 }
1175
1176 let mut dom_node_order_by_root: std::collections::HashMap<String, Vec<String>> =
1180 std::collections::HashMap::new();
1181 let dom_order_start = timing_enabled.then(std::time::Instant::now);
1182 dom_node_order_by_root.insert(String::new(), g.node_ids());
1183 for (id, cg) in &extracted_graphs {
1184 dom_node_order_by_root.insert(id.clone(), cg.node_ids());
1185 }
1186 if let Some(s) = dom_order_start {
1187 timings.dom_order = s.elapsed();
1188 }
1189
1190 type Rect = merman_core::geom::Box2;
1191
1192 fn extracted_graph_bbox_rect(
1193 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1194 title_total_margin: f64,
1195 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1196 subgraph_id_set: &std::collections::HashSet<String>,
1197 ) -> Option<Rect> {
1198 fn graph_content_rect(
1199 g: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1200 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1201 subgraph_id_set: &std::collections::HashSet<String>,
1202 title_total_margin: f64,
1203 ) -> Option<Rect> {
1204 let mut out: Option<Rect> = None;
1205 for id in g.node_ids() {
1206 let Some(n) = g.node(&id) else { continue };
1207 let (Some(x), Some(y)) = (n.x, n.y) else {
1208 continue;
1209 };
1210 let mut height = n.height;
1211 let is_cluster_node = extracted.contains_key(&id) && g.children(&id).is_empty();
1212 let is_non_recursive_cluster =
1213 subgraph_id_set.contains(&id) && !g.children(&id).is_empty();
1214
1215 if !is_cluster_node && is_non_recursive_cluster && title_total_margin > 0.0 {
1222 height = (height + title_total_margin).max(1.0);
1223 }
1224
1225 let r = Rect::from_center(x, y, n.width, height);
1226 if let Some(ref mut cur) = out {
1227 cur.union(r);
1228 } else {
1229 out = Some(r);
1230 }
1231 }
1232 for ek in g.edge_keys() {
1233 let Some(e) = g.edge_by_key(&ek) else {
1234 continue;
1235 };
1236 let (Some(x), Some(y)) = (e.x, e.y) else {
1237 continue;
1238 };
1239 if e.width <= 0.0 && e.height <= 0.0 {
1240 continue;
1241 }
1242 let r = Rect::from_center(x, y, e.width, e.height);
1243 if let Some(ref mut cur) = out {
1244 cur.union(r);
1245 } else {
1246 out = Some(r);
1247 }
1248 }
1249 out
1250 }
1251
1252 graph_content_rect(g, extracted, subgraph_id_set, title_total_margin)
1253 }
1254
1255 fn apply_mermaid_subgraph_title_shifts(
1256 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1257 extracted: &std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1258 subgraph_id_set: &std::collections::HashSet<String>,
1259 y_shift: f64,
1260 ) {
1261 if y_shift.abs() < 1e-9 {
1262 return;
1263 }
1264
1265 for id in graph.node_ids() {
1270 let is_cluster_node = extracted.contains_key(&id) && graph.children(&id).is_empty();
1274 let delta_y = if is_cluster_node {
1275 y_shift * 2.0
1276 } else if subgraph_id_set.contains(&id) && !graph.children(&id).is_empty() {
1277 0.0
1278 } else {
1279 y_shift
1280 };
1281 if delta_y.abs() > 1e-9 {
1282 let Some(y) = graph.node(&id).and_then(|n| n.y) else {
1283 continue;
1284 };
1285 if let Some(n) = graph.node_mut(&id) {
1286 n.y = Some(y + delta_y);
1287 }
1288 }
1289 }
1290
1291 for ek in graph.edge_keys() {
1293 let Some(e) = graph.edge_mut_by_key(&ek) else {
1294 continue;
1295 };
1296 if let Some(y) = e.y {
1297 e.y = Some(y + y_shift);
1298 }
1299 for p in &mut e.points {
1300 p.y += y_shift;
1301 }
1302 }
1303 }
1304
1305 fn layout_graph_with_recursive_clusters(
1306 graph: &mut Graph<NodeLabel, EdgeLabel, GraphLabel>,
1307 graph_cluster_id: Option<&str>,
1308 extracted: &mut std::collections::HashMap<String, Graph<NodeLabel, EdgeLabel, GraphLabel>>,
1309 depth: usize,
1310 subgraph_id_set: &std::collections::HashSet<String>,
1311 y_shift: f64,
1312 cluster_node_labels: &std::collections::HashMap<String, NodeLabel>,
1313 title_total_margin: f64,
1314 timings: &mut FlowchartLayoutTimings,
1315 timing_enabled: bool,
1316 ) {
1317 if depth > 10 {
1318 if timing_enabled {
1319 timings.dagre_calls += 1;
1320 let start = std::time::Instant::now();
1321 dugong::layout_dagreish(graph);
1322 timings.dagre_total += start.elapsed();
1323 } else {
1324 dugong::layout_dagreish(graph);
1325 }
1326 apply_mermaid_subgraph_title_shifts(graph, extracted, subgraph_id_set, y_shift);
1327 return;
1328 }
1329
1330 let ids = graph.node_ids();
1334 for id in ids {
1335 if !extracted.contains_key(&id) {
1336 continue;
1337 }
1338 if !graph.children(&id).is_empty() {
1342 continue;
1343 }
1344 let mut child = match extracted.remove(&id) {
1345 Some(g) => g,
1346 None => continue,
1347 };
1348
1349 let parent_nodesep = graph.graph().nodesep;
1354 let parent_ranksep = graph.graph().ranksep;
1355 child.graph_mut().nodesep = parent_nodesep;
1356 child.graph_mut().ranksep = parent_ranksep + 25.0;
1357
1358 layout_graph_with_recursive_clusters(
1359 &mut child,
1360 Some(id.as_str()),
1361 extracted,
1362 depth + 1,
1363 subgraph_id_set,
1364 y_shift,
1365 cluster_node_labels,
1366 title_total_margin,
1367 timings,
1368 timing_enabled,
1369 );
1370
1371 if let Some(r) =
1376 extracted_graph_bbox_rect(&child, title_total_margin, extracted, subgraph_id_set)
1377 {
1378 if let Some(n) = graph.node_mut(&id) {
1379 n.width = r.width().max(1.0);
1380 n.height = r.height().max(1.0);
1381 }
1382 } else if let Some(n_child) = child.node(&id) {
1383 if let Some(n) = graph.node_mut(&id) {
1384 n.width = n_child.width.max(1.0);
1385 n.height = n_child.height.max(1.0);
1386 }
1387 }
1388 extracted.insert(id, child);
1389 }
1390
1391 if let Some(cluster_id) = graph_cluster_id {
1394 if !graph.has_node(cluster_id) {
1395 let lbl = cluster_node_labels
1396 .get(cluster_id)
1397 .cloned()
1398 .unwrap_or_default();
1399 graph.set_node(cluster_id.to_string(), lbl);
1400 }
1401 let node_ids = graph.node_ids();
1402 for node_id in node_ids {
1403 if node_id == cluster_id {
1404 continue;
1405 }
1406 if graph.parent(&node_id).is_none() {
1407 graph.set_parent(node_id, cluster_id.to_string());
1408 }
1409 }
1410 }
1411
1412 if timing_enabled {
1413 timings.dagre_calls += 1;
1414 let start = std::time::Instant::now();
1415 dugong::layout_dagreish(graph);
1416 timings.dagre_total += start.elapsed();
1417 } else {
1418 dugong::layout_dagreish(graph);
1419 }
1420 apply_mermaid_subgraph_title_shifts(graph, extracted, subgraph_id_set, y_shift);
1421 }
1422
1423 let layout_start = timing_enabled.then(std::time::Instant::now);
1424 layout_graph_with_recursive_clusters(
1425 &mut g,
1426 None,
1427 &mut extracted_graphs,
1428 0,
1429 &subgraph_id_set,
1430 y_shift,
1431 &cluster_node_labels,
1432 title_total_margin,
1433 &mut timings,
1434 timing_enabled,
1435 );
1436 if let Some(s) = layout_start {
1437 timings.layout_recursive = s.elapsed();
1438 }
1439
1440 let mut leaf_rects: std::collections::HashMap<String, Rect> = std::collections::HashMap::new();
1441 let mut base_pos: std::collections::HashMap<String, (f64, f64)> =
1442 std::collections::HashMap::new();
1443 let mut edge_override_points: std::collections::HashMap<String, Vec<LayoutPoint>> =
1444 std::collections::HashMap::new();
1445 let mut edge_override_label: std::collections::HashMap<String, Option<LayoutLabel>> =
1446 std::collections::HashMap::new();
1447 let mut edge_override_from_cluster: std::collections::HashMap<String, Option<String>> =
1448 std::collections::HashMap::new();
1449 let mut edge_override_to_cluster: std::collections::HashMap<String, Option<String>> =
1450 std::collections::HashMap::new();
1451 #[cfg(feature = "flowchart_root_pack")]
1452 let mut edge_packed_shift: std::collections::HashMap<String, (f64, f64)> =
1453 std::collections::HashMap::new();
1454 #[cfg(not(feature = "flowchart_root_pack"))]
1455 let edge_packed_shift: std::collections::HashMap<String, (f64, f64)> =
1456 std::collections::HashMap::new();
1457
1458 let mut leaf_node_ids: std::collections::HashSet<String> = model
1459 .nodes
1460 .iter()
1461 .filter(|n| !subgraph_ids.contains(n.id.as_str()))
1462 .map(|n| n.id.clone())
1463 .collect();
1464 for id in &self_loop_label_node_ids {
1465 leaf_node_ids.insert(id.clone());
1466 }
1467 for id in &empty_subgraph_ids {
1468 leaf_node_ids.insert(id.clone());
1469 }
1470
1471 fn place_graph(
1472 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1473 offset: (f64, f64),
1474 is_root: bool,
1475 edge_id_by_key: &std::collections::HashMap<String, String>,
1476 extracted_graphs: &std::collections::HashMap<
1477 String,
1478 Graph<NodeLabel, EdgeLabel, GraphLabel>,
1479 >,
1480 subgraph_ids: &std::collections::HashSet<&str>,
1481 leaf_node_ids: &std::collections::HashSet<String>,
1482 _title_total_margin: f64,
1483 base_pos: &mut std::collections::HashMap<String, (f64, f64)>,
1484 leaf_rects: &mut std::collections::HashMap<String, Rect>,
1485 cluster_rects_from_graph: &mut std::collections::HashMap<String, Rect>,
1486 extracted_cluster_rects: &mut std::collections::HashMap<String, Rect>,
1487 edge_override_points: &mut std::collections::HashMap<String, Vec<LayoutPoint>>,
1488 edge_override_label: &mut std::collections::HashMap<String, Option<LayoutLabel>>,
1489 edge_override_from_cluster: &mut std::collections::HashMap<String, Option<String>>,
1490 edge_override_to_cluster: &mut std::collections::HashMap<String, Option<String>>,
1491 ) {
1492 for id in graph.node_ids() {
1493 let Some(n) = graph.node(&id) else { continue };
1494 let x = n.x.unwrap_or(0.0) + offset.0;
1495 let y = n.y.unwrap_or(0.0) + offset.1;
1496 if leaf_node_ids.contains(&id) {
1497 base_pos.insert(id.clone(), (x, y));
1498 leaf_rects.insert(id.clone(), Rect::from_center(x, y, n.width, n.height));
1499 continue;
1500 }
1501 }
1502
1503 fn subtree_rect(
1504 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1505 id: &str,
1506 visiting: &mut std::collections::HashSet<String>,
1507 ) -> Option<Rect> {
1508 if !visiting.insert(id.to_string()) {
1509 return None;
1510 }
1511 let mut out: Option<Rect> = None;
1512 for child in graph.children(id) {
1513 if let Some(n) = graph.node(child) {
1514 if let (Some(x), Some(y)) = (n.x, n.y) {
1515 let r = Rect::from_center(x, y, n.width, n.height);
1516 if let Some(ref mut cur) = out {
1517 cur.union(r);
1518 } else {
1519 out = Some(r);
1520 }
1521 }
1522 }
1523 if !graph.children(child).is_empty() {
1524 if let Some(r) = subtree_rect(graph, child, visiting) {
1525 if let Some(ref mut cur) = out {
1526 cur.union(r);
1527 } else {
1528 out = Some(r);
1529 }
1530 }
1531 }
1532 }
1533 visiting.remove(id);
1534 out
1535 }
1536
1537 for id in graph.node_ids() {
1543 if !subgraph_ids.contains(id.as_str()) {
1544 continue;
1545 }
1546 if extracted_graphs.contains_key(&id) {
1547 continue;
1548 }
1549 if cluster_rects_from_graph.contains_key(&id) {
1550 continue;
1551 }
1552 if let Some(n) = graph.node(&id) {
1553 if let (Some(x), Some(y)) = (n.x, n.y) {
1554 if n.width > 0.0 && n.height > 0.0 {
1555 let mut r = Rect::from_center(x, y, n.width, n.height);
1556 r.translate(offset.0, offset.1);
1557 cluster_rects_from_graph.insert(id, r);
1558 continue;
1559 }
1560 }
1561 }
1562
1563 let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
1564 let Some(mut r) = subtree_rect(graph, &id, &mut visiting) else {
1565 continue;
1566 };
1567 r.translate(offset.0, offset.1);
1568 cluster_rects_from_graph.insert(id, r);
1569 }
1570
1571 for ek in graph.edge_keys() {
1572 let Some(edge_key) = ek.name.as_deref() else {
1573 continue;
1574 };
1575 let edge_id = edge_id_by_key
1576 .get(edge_key)
1577 .map(String::as_str)
1578 .unwrap_or(edge_key);
1579 let Some(lbl) = graph.edge_by_key(&ek) else {
1580 continue;
1581 };
1582
1583 if let (Some(x), Some(y)) = (lbl.x, lbl.y) {
1584 if lbl.width > 0.0 || lbl.height > 0.0 {
1585 let lx = x + offset.0;
1586 let ly = y + offset.1;
1587 let leaf_id = format!("edge-label::{edge_id}");
1588 base_pos.insert(leaf_id.clone(), (lx, ly));
1589 leaf_rects.insert(leaf_id, Rect::from_center(lx, ly, lbl.width, lbl.height));
1590 }
1591 }
1592
1593 if !is_root {
1594 let points = lbl
1595 .points
1596 .iter()
1597 .map(|p| LayoutPoint {
1598 x: p.x + offset.0,
1599 y: p.y + offset.1,
1600 })
1601 .collect::<Vec<_>>();
1602 let label_pos = match (lbl.x, lbl.y) {
1603 (Some(x), Some(y)) if lbl.width > 0.0 || lbl.height > 0.0 => {
1604 Some(LayoutLabel {
1605 x: x + offset.0,
1606 y: y + offset.1,
1607 width: lbl.width,
1608 height: lbl.height,
1609 })
1610 }
1611 _ => None,
1612 };
1613 edge_override_points.insert(edge_id.to_string(), points);
1614 edge_override_label.insert(edge_id.to_string(), label_pos);
1615 let from_cluster = lbl
1616 .extras
1617 .get("fromCluster")
1618 .and_then(|v| v.as_str().map(|s| s.to_string()));
1619 let to_cluster = lbl
1620 .extras
1621 .get("toCluster")
1622 .and_then(|v| v.as_str().map(|s| s.to_string()));
1623 edge_override_from_cluster.insert(edge_id.to_string(), from_cluster);
1624 edge_override_to_cluster.insert(edge_id.to_string(), to_cluster);
1625 }
1626 }
1627
1628 for id in graph.node_ids() {
1629 if !graph.children(&id).is_empty() {
1633 continue;
1634 }
1635 let Some(child) = extracted_graphs.get(&id) else {
1636 continue;
1637 };
1638 let Some(n) = graph.node(&id) else {
1639 continue;
1640 };
1641 let (Some(px), Some(py)) = (n.x, n.y) else {
1642 continue;
1643 };
1644 let parent_x = px + offset.0;
1645 let parent_y = py + offset.1;
1646 let Some(cnode) = child.node(&id) else {
1647 continue;
1648 };
1649 let (Some(cx), Some(cy)) = (cnode.x, cnode.y) else {
1650 continue;
1651 };
1652 let child_offset = (parent_x - cx, parent_y - cy);
1653 let r = Rect::from_center(parent_x, parent_y, n.width, n.height);
1658 extracted_cluster_rects.insert(id.clone(), r);
1659 place_graph(
1660 child,
1661 child_offset,
1662 false,
1663 edge_id_by_key,
1664 extracted_graphs,
1665 subgraph_ids,
1666 leaf_node_ids,
1667 _title_total_margin,
1668 base_pos,
1669 leaf_rects,
1670 cluster_rects_from_graph,
1671 extracted_cluster_rects,
1672 edge_override_points,
1673 edge_override_label,
1674 edge_override_from_cluster,
1675 edge_override_to_cluster,
1676 );
1677 }
1678 }
1679
1680 let mut cluster_rects_from_graph: std::collections::HashMap<String, Rect> =
1681 std::collections::HashMap::new();
1682 let mut extracted_cluster_rects: std::collections::HashMap<String, Rect> =
1683 std::collections::HashMap::new();
1684 let place_start = timing_enabled.then(std::time::Instant::now);
1685 place_graph(
1686 &g,
1687 (0.0, 0.0),
1688 true,
1689 &edge_id_by_key,
1690 &extracted_graphs,
1691 &subgraph_ids,
1692 &leaf_node_ids,
1693 title_total_margin,
1694 &mut base_pos,
1695 &mut leaf_rects,
1696 &mut cluster_rects_from_graph,
1697 &mut extracted_cluster_rects,
1698 &mut edge_override_points,
1699 &mut edge_override_label,
1700 &mut edge_override_from_cluster,
1701 &mut edge_override_to_cluster,
1702 );
1703 if let Some(s) = place_start {
1704 timings.place_graph = s.elapsed();
1705 }
1706
1707 let build_output_start = timing_enabled.then(std::time::Instant::now);
1708
1709 let mut extra_children: std::collections::HashMap<String, Vec<String>> =
1710 std::collections::HashMap::new();
1711 let labeled_edges: std::collections::HashSet<&str> = render_edges
1712 .iter()
1713 .filter(|e| edge_label_is_non_empty(e))
1714 .map(|e| e.id.as_str())
1715 .collect();
1716
1717 fn collect_extra_children(
1718 graph: &Graph<NodeLabel, EdgeLabel, GraphLabel>,
1719 edge_id_by_key: &std::collections::HashMap<String, String>,
1720 labeled_edges: &std::collections::HashSet<&str>,
1721 implicit_root: Option<&str>,
1722 out: &mut std::collections::HashMap<String, Vec<String>>,
1723 ) {
1724 for ek in graph.edge_keys() {
1725 let Some(edge_key) = ek.name.as_deref() else {
1726 continue;
1727 };
1728 let edge_id = edge_id_by_key
1729 .get(edge_key)
1730 .map(String::as_str)
1731 .unwrap_or(edge_key);
1732 if !labeled_edges.contains(edge_id) {
1733 continue;
1734 }
1735 let parent = lowest_common_parent(graph, &ek.v, &ek.w)
1741 .or_else(|| implicit_root.map(|s| s.to_string()));
1742 let Some(parent) = parent else {
1743 continue;
1744 };
1745 out.entry(parent)
1746 .or_default()
1747 .push(format!("edge-label::{edge_id}"));
1748 }
1749 }
1750
1751 collect_extra_children(
1752 &g,
1753 &edge_id_by_key,
1754 &labeled_edges,
1755 None,
1756 &mut extra_children,
1757 );
1758 for (cluster_id, cg) in &extracted_graphs {
1759 collect_extra_children(
1760 cg,
1761 &edge_id_by_key,
1762 &labeled_edges,
1763 Some(cluster_id.as_str()),
1764 &mut extra_children,
1765 );
1766 }
1767
1768 for id in &self_loop_label_node_ids {
1772 if let Some(p) = g.parent(id) {
1773 extra_children
1774 .entry(p.to_string())
1775 .or_default()
1776 .push(id.clone());
1777 }
1778 }
1779
1780 #[cfg(feature = "flowchart_root_pack")]
1783 {
1784 let subgraph_ids: std::collections::HashSet<&str> =
1785 model.subgraphs.iter().map(|s| s.id.as_str()).collect();
1786
1787 let mut subgraph_has_parent: std::collections::HashSet<&str> =
1788 std::collections::HashSet::new();
1789 for sg in &model.subgraphs {
1790 for child in &sg.nodes {
1791 if subgraph_ids.contains(child.as_str()) {
1792 subgraph_has_parent.insert(child.as_str());
1793 }
1794 }
1795 }
1796
1797 fn collect_descendant_leaf_nodes<'a>(
1798 id: &'a str,
1799 subgraphs_by_id: &'a std::collections::HashMap<String, FlowSubgraph>,
1800 subgraph_ids: &std::collections::HashSet<&'a str>,
1801 out: &mut std::collections::HashSet<String>,
1802 visiting: &mut std::collections::HashSet<&'a str>,
1803 ) {
1804 if !visiting.insert(id) {
1805 return;
1806 }
1807 let Some(sg) = subgraphs_by_id.get(id) else {
1808 visiting.remove(id);
1809 return;
1810 };
1811 for member in &sg.nodes {
1812 if subgraph_ids.contains(member.as_str()) {
1813 collect_descendant_leaf_nodes(
1814 member,
1815 subgraphs_by_id,
1816 subgraph_ids,
1817 out,
1818 visiting,
1819 );
1820 } else {
1821 out.insert(member.clone());
1822 }
1823 }
1824 visiting.remove(id);
1825 }
1826
1827 fn collect_descendant_cluster_ids<'a>(
1828 id: &'a str,
1829 subgraphs_by_id: &'a std::collections::HashMap<String, FlowSubgraph>,
1830 subgraph_ids: &std::collections::HashSet<&'a str>,
1831 out: &mut std::collections::HashSet<String>,
1832 visiting: &mut std::collections::HashSet<&'a str>,
1833 ) {
1834 if !visiting.insert(id) {
1835 return;
1836 }
1837 let Some(sg) = subgraphs_by_id.get(id) else {
1838 visiting.remove(id);
1839 return;
1840 };
1841 out.insert(id.to_string());
1842 for member in &sg.nodes {
1843 if subgraph_ids.contains(member.as_str()) {
1844 collect_descendant_cluster_ids(
1845 member,
1846 subgraphs_by_id,
1847 subgraph_ids,
1848 out,
1849 visiting,
1850 );
1851 }
1852 }
1853 visiting.remove(id);
1854 }
1855
1856 fn has_external_edges(
1857 leaves: &std::collections::HashSet<String>,
1858 edges: &[FlowEdge],
1859 ) -> bool {
1860 for e in edges {
1861 let in_from = leaves.contains(&e.from);
1862 let in_to = leaves.contains(&e.to);
1863 if in_from ^ in_to {
1864 return true;
1865 }
1866 }
1867 false
1868 }
1869
1870 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1871 enum PackAxis {
1872 X,
1873 Y,
1874 }
1875
1876 let pack_axis = match diagram_direction.as_str() {
1877 "LR" | "RL" => PackAxis::Y,
1878 _ => PackAxis::X,
1879 };
1880
1881 let mut pack_rects: std::collections::HashMap<String, Rect> =
1882 std::collections::HashMap::new();
1883 let mut pack_visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
1884
1885 fn compute_pack_rect(
1886 id: &str,
1887 subgraphs_by_id: &std::collections::HashMap<String, FlowSubgraph>,
1888 leaf_rects: &std::collections::HashMap<String, Rect>,
1889 extra_children: &std::collections::HashMap<String, Vec<String>>,
1890 extracted_cluster_rects: &std::collections::HashMap<String, Rect>,
1891 pack_rects: &mut std::collections::HashMap<String, Rect>,
1892 pack_visiting: &mut std::collections::HashSet<String>,
1893 measurer: &dyn TextMeasurer,
1894 text_style: &TextStyle,
1895 title_wrapping_width: f64,
1896 wrap_mode: WrapMode,
1897 cluster_padding: f64,
1898 title_total_margin: f64,
1899 node_padding: f64,
1900 ) -> Option<Rect> {
1901 if let Some(r) = pack_rects.get(id).copied() {
1902 return Some(r);
1903 }
1904 if !pack_visiting.insert(id.to_string()) {
1905 return None;
1906 }
1907 if let Some(r) = extracted_cluster_rects.get(id).copied() {
1908 pack_visiting.remove(id);
1909 pack_rects.insert(id.to_string(), r);
1910 return Some(r);
1911 }
1912 let Some(sg) = subgraphs_by_id.get(id) else {
1913 pack_visiting.remove(id);
1914 return None;
1915 };
1916
1917 let mut content: Option<Rect> = None;
1918 for member in &sg.nodes {
1919 let member_rect = if let Some(r) = leaf_rects.get(member).copied() {
1920 Some(r)
1921 } else if subgraphs_by_id.contains_key(member) {
1922 compute_pack_rect(
1923 member,
1924 subgraphs_by_id,
1925 leaf_rects,
1926 extra_children,
1927 extracted_cluster_rects,
1928 pack_rects,
1929 pack_visiting,
1930 measurer,
1931 text_style,
1932 title_wrapping_width,
1933 wrap_mode,
1934 cluster_padding,
1935 title_total_margin,
1936 node_padding,
1937 )
1938 } else {
1939 None
1940 };
1941
1942 if let Some(r) = member_rect {
1943 if let Some(ref mut cur) = content {
1944 cur.union(r);
1945 } else {
1946 content = Some(r);
1947 }
1948 }
1949 }
1950
1951 if let Some(extra) = extra_children.get(id) {
1952 for child in extra {
1953 if let Some(r) = leaf_rects.get(child).copied() {
1954 if let Some(ref mut cur) = content {
1955 cur.union(r);
1956 } else {
1957 content = Some(r);
1958 }
1959 }
1960 }
1961 }
1962
1963 let label_type = sg.label_type.as_deref().unwrap_or("text");
1964 let title_width_limit = Some(title_wrapping_width);
1965 let title_metrics = flowchart_label_metrics_for_layout(
1966 measurer,
1967 &sg.title,
1968 label_type,
1969 text_style,
1970 title_width_limit,
1971 wrap_mode,
1972 effective_config,
1973 math_renderer,
1974 );
1975
1976 let mut rect = if let Some(r) = content {
1977 r
1978 } else {
1979 Rect::from_center(
1980 0.0,
1981 0.0,
1982 title_metrics.width.max(1.0),
1983 title_metrics.height.max(1.0),
1984 )
1985 };
1986
1987 rect.min_x -= cluster_padding;
1988 rect.max_x += cluster_padding;
1989 rect.min_y -= cluster_padding;
1990 rect.max_y += cluster_padding;
1991
1992 let min_width = title_metrics.width.max(1.0) + cluster_padding;
1996 if rect.width() < min_width {
1997 let (cx, cy) = rect.center();
1998 rect = Rect::from_center(cx, cy, min_width, rect.height());
1999 }
2000
2001 if title_total_margin > 0.0 {
2002 let (cx, cy) = rect.center();
2003 rect = Rect::from_center(cx, cy, rect.width(), rect.height() + title_total_margin);
2004 }
2005
2006 let min_height = title_metrics.height.max(1.0) + title_total_margin;
2007 if rect.height() < min_height {
2008 let (cx, cy) = rect.center();
2009 rect = Rect::from_center(cx, cy, rect.width(), min_height);
2010 }
2011
2012 pack_visiting.remove(id);
2013 pack_rects.insert(id.to_string(), rect);
2014 Some(rect)
2015 }
2016
2017 struct PackItem {
2018 rect: Rect,
2019 members: Vec<String>,
2020 internal_edge_ids: Vec<String>,
2021 cluster_ids: Vec<String>,
2022 }
2023
2024 let mut items: Vec<PackItem> = Vec::new();
2025 for sg in &model.subgraphs {
2026 if subgraph_has_parent.contains(sg.id.as_str()) {
2027 continue;
2028 }
2029
2030 let mut leaves: std::collections::HashSet<String> = std::collections::HashSet::new();
2031 let mut visiting: std::collections::HashSet<&str> = std::collections::HashSet::new();
2032 collect_descendant_leaf_nodes(
2033 &sg.id,
2034 &subgraphs_by_id,
2035 &subgraph_ids,
2036 &mut leaves,
2037 &mut visiting,
2038 );
2039 if leaves.is_empty() {
2040 continue;
2041 }
2042
2043 let mut cluster_ids_set: std::collections::HashSet<String> =
2049 std::collections::HashSet::new();
2050 let mut cluster_visiting: std::collections::HashSet<&str> =
2051 std::collections::HashSet::new();
2052 collect_descendant_cluster_ids(
2053 &sg.id,
2054 &subgraphs_by_id,
2055 &subgraph_ids,
2056 &mut cluster_ids_set,
2057 &mut cluster_visiting,
2058 );
2059
2060 let mut membership_endpoints: std::collections::HashSet<String> =
2061 std::collections::HashSet::new();
2062 membership_endpoints.extend(leaves.iter().cloned());
2063 membership_endpoints.extend(cluster_ids_set.iter().cloned());
2064
2065 if has_external_edges(&membership_endpoints, &render_edges) {
2066 continue;
2067 }
2068
2069 let Some(rect) = compute_pack_rect(
2070 &sg.id,
2071 &subgraphs_by_id,
2072 &leaf_rects,
2073 &extra_children,
2074 &extracted_cluster_rects,
2075 &mut pack_rects,
2076 &mut pack_visiting,
2077 measurer,
2078 &text_style,
2079 cluster_title_wrapping_width,
2080 node_wrap_mode,
2081 cluster_padding,
2082 title_total_margin,
2083 node_padding,
2084 ) else {
2085 continue;
2086 };
2087
2088 let mut members = leaves.iter().cloned().collect::<Vec<_>>();
2089 if let Some(extra) = extra_children.get(&sg.id) {
2090 members.extend(extra.iter().cloned());
2091 }
2092
2093 let mut internal_edge_ids: Vec<String> = Vec::new();
2095 for e in &render_edges {
2096 if leaves.contains(&e.from) && leaves.contains(&e.to) {
2097 internal_edge_ids.push(e.id.clone());
2098 if edge_label_is_non_empty(e) {
2099 members.push(edge_label_leaf_id(e));
2100 }
2101 }
2102 }
2103
2104 let mut cluster_ids = cluster_ids_set.into_iter().collect::<Vec<_>>();
2105 cluster_ids.sort();
2106
2107 items.push(PackItem {
2108 rect,
2109 members,
2110 internal_edge_ids,
2111 cluster_ids,
2112 });
2113 }
2114
2115 if !items.is_empty() {
2116 items.sort_by(|a, b| match pack_axis {
2117 PackAxis::X => a.rect.min_x.total_cmp(&b.rect.min_x),
2118 PackAxis::Y => a.rect.min_y.total_cmp(&b.rect.min_y),
2119 });
2120
2121 let mut cursor = match pack_axis {
2122 PackAxis::X => items.first().unwrap().rect.min_x,
2123 PackAxis::Y => items.first().unwrap().rect.min_y,
2124 };
2125
2126 for item in items {
2127 let (cx, cy) = item.rect.center();
2128 let desired_center = match pack_axis {
2129 PackAxis::X => cursor + item.rect.width() / 2.0,
2130 PackAxis::Y => cursor + item.rect.height() / 2.0,
2131 };
2132 let (dx, dy) = match pack_axis {
2133 PackAxis::X => (desired_center - cx, 0.0),
2134 PackAxis::Y => (0.0, desired_center - cy),
2135 };
2136
2137 if dx.abs() > 1e-6 || dy.abs() > 1e-6 {
2138 for id in &item.members {
2139 if let Some((x, y)) = base_pos.get_mut(id) {
2140 *x += dx;
2141 *y += dy;
2142 }
2143 if let Some(r) = leaf_rects.get_mut(id) {
2144 r.translate(dx, dy);
2145 }
2146 }
2147 for cid in &item.cluster_ids {
2148 if let Some(r) = extracted_cluster_rects.get_mut(cid) {
2149 r.translate(dx, dy);
2150 }
2151 if let Some(r) = cluster_rects_from_graph.get_mut(cid) {
2152 r.translate(dx, dy);
2153 }
2154 }
2155 for edge_id in &item.internal_edge_ids {
2156 edge_packed_shift.insert(edge_id.clone(), (dx, dy));
2157 }
2158 }
2159
2160 cursor += match pack_axis {
2161 PackAxis::X => item.rect.width() + nodesep,
2162 PackAxis::Y => item.rect.height() + nodesep,
2163 };
2164 }
2165 }
2166 }
2167
2168 let mut out_nodes: Vec<LayoutNode> = Vec::new();
2169 for n in &model.nodes {
2170 if subgraph_ids.contains(n.id.as_str()) {
2171 continue;
2172 }
2173 let (x, y) = base_pos
2174 .get(&n.id)
2175 .copied()
2176 .ok_or_else(|| Error::InvalidModel {
2177 message: format!("missing positioned node {}", n.id),
2178 })?;
2179 let (width, height) = leaf_rects
2180 .get(&n.id)
2181 .map(|r| (r.width(), r.height()))
2182 .ok_or_else(|| Error::InvalidModel {
2183 message: format!("missing sized node {}", n.id),
2184 })?;
2185 out_nodes.push(LayoutNode {
2186 id: n.id.clone(),
2187 x,
2188 y,
2189 width,
2190 height,
2191 is_cluster: false,
2192 label_width: leaf_label_metrics_by_id.get(&n.id).map(|v| v.0),
2193 label_height: leaf_label_metrics_by_id.get(&n.id).map(|v| v.1),
2194 });
2195 }
2196 for id in &empty_subgraph_ids {
2197 let (x, y) = base_pos
2198 .get(id)
2199 .copied()
2200 .ok_or_else(|| Error::InvalidModel {
2201 message: format!("missing positioned node {id}"),
2202 })?;
2203 let (width, height) = leaf_rects
2204 .get(id)
2205 .map(|r| (r.width(), r.height()))
2206 .ok_or_else(|| Error::InvalidModel {
2207 message: format!("missing sized node {id}"),
2208 })?;
2209 out_nodes.push(LayoutNode {
2210 id: id.clone(),
2211 x,
2212 y,
2213 width,
2214 height,
2215 is_cluster: false,
2216 label_width: leaf_label_metrics_by_id.get(id).map(|v| v.0),
2217 label_height: leaf_label_metrics_by_id.get(id).map(|v| v.1),
2218 });
2219 }
2220 for id in &self_loop_label_node_ids {
2221 let (x, y) = base_pos
2222 .get(id)
2223 .copied()
2224 .ok_or_else(|| Error::InvalidModel {
2225 message: format!("missing positioned node {id}"),
2226 })?;
2227 let (width, height) = leaf_rects
2228 .get(id)
2229 .map(|r| (r.width(), r.height()))
2230 .ok_or_else(|| Error::InvalidModel {
2231 message: format!("missing sized node {id}"),
2232 })?;
2233 out_nodes.push(LayoutNode {
2234 id: id.clone(),
2235 x,
2236 y,
2237 width,
2238 height,
2239 is_cluster: false,
2240 label_width: None,
2241 label_height: None,
2242 });
2243 }
2244
2245 let mut clusters: Vec<LayoutCluster> = Vec::new();
2246
2247 let mut cluster_rects: std::collections::HashMap<String, Rect> =
2248 std::collections::HashMap::new();
2249 let mut cluster_base_widths: std::collections::HashMap<String, f64> =
2250 std::collections::HashMap::new();
2251 let mut visiting: std::collections::HashSet<String> = std::collections::HashSet::new();
2252
2253 fn compute_cluster_rect(
2254 id: &str,
2255 subgraphs_by_id: &std::collections::HashMap<String, FlowSubgraph>,
2256 leaf_rects: &std::collections::HashMap<String, Rect>,
2257 extra_children: &std::collections::HashMap<String, Vec<String>>,
2258 cluster_rects: &mut std::collections::HashMap<String, Rect>,
2259 cluster_base_widths: &mut std::collections::HashMap<String, f64>,
2260 visiting: &mut std::collections::HashSet<String>,
2261 measurer: &dyn TextMeasurer,
2262 text_style: &TextStyle,
2263 title_wrapping_width: f64,
2264 wrap_mode: WrapMode,
2265 config: &MermaidConfig,
2266 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
2267 cluster_padding: f64,
2268 title_total_margin: f64,
2269 _node_padding: f64,
2270 ) -> Result<(Rect, f64)> {
2271 if let Some(r) = cluster_rects.get(id).copied() {
2272 let base_width = cluster_base_widths
2273 .get(id)
2274 .copied()
2275 .unwrap_or_else(|| r.width());
2276 return Ok((r, base_width));
2277 }
2278 if !visiting.insert(id.to_string()) {
2279 return Err(Error::InvalidModel {
2280 message: format!("cycle in subgraph membership involving {id}"),
2281 });
2282 }
2283
2284 let Some(sg) = subgraphs_by_id.get(id) else {
2285 return Err(Error::InvalidModel {
2286 message: format!("missing subgraph definition for {id}"),
2287 });
2288 };
2289
2290 let mut content: Option<Rect> = None;
2291 for member in &sg.nodes {
2292 let member_rect = if let Some(r) = leaf_rects.get(member).copied() {
2293 Some(r)
2294 } else if subgraphs_by_id.contains_key(member) {
2295 Some(
2296 compute_cluster_rect(
2297 member,
2298 subgraphs_by_id,
2299 leaf_rects,
2300 extra_children,
2301 cluster_rects,
2302 cluster_base_widths,
2303 visiting,
2304 measurer,
2305 text_style,
2306 title_wrapping_width,
2307 wrap_mode,
2308 config,
2309 math_renderer,
2310 cluster_padding,
2311 title_total_margin,
2312 _node_padding,
2313 )?
2314 .0,
2315 )
2316 } else {
2317 None
2318 };
2319
2320 if let Some(r) = member_rect {
2321 if let Some(ref mut cur) = content {
2322 cur.union(r);
2323 } else {
2324 content = Some(r);
2325 }
2326 }
2327 }
2328
2329 if let Some(extra) = extra_children.get(id) {
2330 for child in extra {
2331 if let Some(r) = leaf_rects.get(child).copied() {
2332 if let Some(ref mut cur) = content {
2333 cur.union(r);
2334 } else {
2335 content = Some(r);
2336 }
2337 }
2338 }
2339 }
2340
2341 let label_type = sg.label_type.as_deref().unwrap_or("text");
2342 let title_width_limit = Some(title_wrapping_width);
2343 let title_metrics = flowchart_label_metrics_for_layout(
2344 measurer,
2345 &sg.title,
2346 label_type,
2347 text_style,
2348 title_width_limit,
2349 wrap_mode,
2350 config,
2351 math_renderer,
2352 );
2353 let mut rect = if let Some(r) = content {
2354 r
2355 } else {
2356 Rect::from_center(
2357 0.0,
2358 0.0,
2359 title_metrics.width.max(1.0),
2360 title_metrics.height.max(1.0),
2361 )
2362 };
2363
2364 rect.pad(cluster_padding);
2366
2367 let base_width = rect.width();
2370
2371 let min_width = title_metrics.width.max(1.0) + cluster_padding;
2375 if rect.width() < min_width {
2376 let (cx, cy) = rect.center();
2377 rect = Rect::from_center(cx, cy, min_width, rect.height());
2378 }
2379
2380 if title_total_margin > 0.0 {
2382 let (cx, cy) = rect.center();
2383 rect = Rect::from_center(cx, cy, rect.width(), rect.height() + title_total_margin);
2384 }
2385
2386 let min_height = title_metrics.height.max(1.0) + title_total_margin;
2388 if rect.height() < min_height {
2389 let (cx, cy) = rect.center();
2390 rect = Rect::from_center(cx, cy, rect.width(), min_height);
2391 }
2392
2393 visiting.remove(id);
2394 cluster_rects.insert(id.to_string(), rect);
2395 cluster_base_widths.insert(id.to_string(), base_width);
2396 Ok((rect, base_width))
2397 }
2398
2399 for sg in &model.subgraphs {
2400 fn adjust_cluster_rect_for_title(
2401 mut rect: Rect,
2402 title: &str,
2403 label_type: &str,
2404 measurer: &dyn TextMeasurer,
2405 text_style: &TextStyle,
2406 title_wrapping_width: f64,
2407 wrap_mode: WrapMode,
2408 config: &MermaidConfig,
2409 math_renderer: Option<&(dyn MathRenderer + Send + Sync)>,
2410 title_total_margin: f64,
2411 cluster_padding: f64,
2412 add_title_total_margin: bool,
2413 ) -> Rect {
2414 let title_width_limit = Some(title_wrapping_width);
2415 let title_metrics = flowchart_label_metrics_for_layout(
2416 measurer,
2417 title,
2418 label_type,
2419 text_style,
2420 title_width_limit,
2421 wrap_mode,
2422 config,
2423 math_renderer,
2424 );
2425 let title_w = title_metrics.width.max(1.0);
2426 let title_h = title_metrics.height.max(1.0);
2427
2428 let min_w = title_w + cluster_padding;
2431 if rect.width() < min_w {
2432 let (cx, cy) = rect.center();
2433 rect = Rect::from_center(cx, cy, min_w, rect.height());
2434 }
2435
2436 if add_title_total_margin && title_total_margin > 0.0 {
2438 let (cx, cy) = rect.center();
2439 rect = Rect::from_center(cx, cy, rect.width(), rect.height() + title_total_margin);
2440 }
2441
2442 let min_h = title_h + title_total_margin;
2444 if rect.height() < min_h {
2445 let (cx, cy) = rect.center();
2446 rect = Rect::from_center(cx, cy, rect.width(), min_h);
2447 }
2448
2449 rect
2450 }
2451
2452 if sg.nodes.is_empty() {
2453 continue;
2454 }
2455
2456 let (rect, base_width) = if extracted_graphs.contains_key(&sg.id) {
2457 let rect = extracted_cluster_rects
2461 .get(&sg.id)
2462 .copied()
2463 .unwrap_or_else(|| {
2464 compute_cluster_rect(
2465 &sg.id,
2466 &subgraphs_by_id,
2467 &leaf_rects,
2468 &extra_children,
2469 &mut cluster_rects,
2470 &mut cluster_base_widths,
2471 &mut visiting,
2472 measurer,
2473 &text_style,
2474 cluster_title_wrapping_width,
2475 cluster_wrap_mode,
2476 effective_config,
2477 math_renderer,
2478 cluster_padding,
2479 title_total_margin,
2480 node_padding,
2481 )
2482 .map(|v| v.0)
2483 .unwrap_or_else(|_| Rect::from_center(0.0, 0.0, 1.0, 1.0))
2484 });
2485 let base_width = rect.width();
2486 let rect = adjust_cluster_rect_for_title(
2487 rect,
2488 &sg.title,
2489 sg.label_type.as_deref().unwrap_or("text"),
2490 measurer,
2491 &text_style,
2492 cluster_title_wrapping_width,
2493 cluster_wrap_mode,
2494 effective_config,
2495 math_renderer,
2496 title_total_margin,
2497 cluster_padding,
2498 false,
2499 );
2500 (rect, base_width)
2501 } else if let Some(r) = cluster_rects_from_graph.get(&sg.id).copied() {
2502 let base_width = r.width();
2503 let rect = adjust_cluster_rect_for_title(
2504 r,
2505 &sg.title,
2506 sg.label_type.as_deref().unwrap_or("text"),
2507 measurer,
2508 &text_style,
2509 cluster_title_wrapping_width,
2510 cluster_wrap_mode,
2511 effective_config,
2512 math_renderer,
2513 title_total_margin,
2514 cluster_padding,
2515 true,
2516 );
2517 (rect, base_width)
2518 } else {
2519 compute_cluster_rect(
2520 &sg.id,
2521 &subgraphs_by_id,
2522 &leaf_rects,
2523 &extra_children,
2524 &mut cluster_rects,
2525 &mut cluster_base_widths,
2526 &mut visiting,
2527 measurer,
2528 &text_style,
2529 cluster_title_wrapping_width,
2530 cluster_wrap_mode,
2531 effective_config,
2532 math_renderer,
2533 cluster_padding,
2534 title_total_margin,
2535 node_padding,
2536 )?
2537 };
2538 let (cx, cy) = rect.center();
2539
2540 let label_type = sg.label_type.as_deref().unwrap_or("text");
2541 let title_width_limit = Some(cluster_title_wrapping_width);
2542 let title_metrics = flowchart_label_metrics_for_layout(
2543 measurer,
2544 &sg.title,
2545 label_type,
2546 &text_style,
2547 title_width_limit,
2548 cluster_wrap_mode,
2549 effective_config,
2550 math_renderer,
2551 );
2552 let title_label = LayoutLabel {
2553 x: cx,
2554 y: cy - rect.height() / 2.0 + title_margin_top + title_metrics.height / 2.0,
2555 width: title_metrics.width,
2556 height: title_metrics.height,
2557 };
2558
2559 let title_w = title_metrics.width.max(1.0);
2564 let diff = if base_width <= title_w {
2565 (title_w - base_width) / 2.0 - cluster_padding / 2.0
2566 } else {
2567 -cluster_padding / 2.0
2568 };
2569 let offset_y = title_metrics.height - cluster_padding / 2.0;
2570
2571 let effective_dir = effective_dir_by_id
2572 .get(&sg.id)
2573 .cloned()
2574 .unwrap_or_else(|| effective_cluster_dir(sg, &diagram_direction, inherit_dir));
2575
2576 clusters.push(LayoutCluster {
2577 id: sg.id.clone(),
2578 x: cx,
2579 y: cy,
2580 width: rect.width(),
2581 height: rect.height(),
2582 diff,
2583 offset_y,
2584 title: sg.title.clone(),
2585 title_label,
2586 requested_dir: sg.dir.as_ref().map(|s| normalize_dir(s)),
2587 effective_dir,
2588 padding: cluster_padding,
2589 title_margin_top,
2590 title_margin_bottom,
2591 });
2592
2593 out_nodes.push(LayoutNode {
2594 id: sg.id.clone(),
2595 x: cx,
2596 y: cy,
2598 width: rect.width(),
2599 height: rect.height(),
2600 is_cluster: true,
2601 label_width: None,
2602 label_height: None,
2603 });
2604 }
2605 clusters.sort_by(|a, b| a.id.cmp(&b.id));
2606
2607 let mut out_edges: Vec<LayoutEdge> = Vec::new();
2608 for e in &render_edges {
2609 let (dx, dy) = edge_packed_shift.get(&e.id).copied().unwrap_or((0.0, 0.0));
2610 let (
2611 mut points,
2612 mut label_pos,
2613 label_pos_already_shifted,
2614 mut from_cluster,
2615 mut to_cluster,
2616 ) = if let Some(points) = edge_override_points.get(&e.id) {
2617 let from_cluster = edge_override_from_cluster
2618 .get(&e.id)
2619 .cloned()
2620 .unwrap_or(None);
2621 let to_cluster = edge_override_to_cluster.get(&e.id).cloned().unwrap_or(None);
2622 (
2623 points.clone(),
2624 edge_override_label.get(&e.id).cloned().unwrap_or(None),
2625 false,
2626 from_cluster,
2627 to_cluster,
2628 )
2629 } else {
2630 let (v, w) = edge_endpoints_by_id
2631 .get(&e.id)
2632 .cloned()
2633 .unwrap_or_else(|| (e.from.clone(), e.to.clone()));
2634 let edge_key = edge_key_by_id
2635 .get(&e.id)
2636 .map(String::as_str)
2637 .unwrap_or(e.id.as_str());
2638 let Some(label) = g.edge(&v, &w, Some(edge_key)) else {
2639 return Err(Error::InvalidModel {
2640 message: format!("missing layout edge {}", e.id),
2641 });
2642 };
2643 let from_cluster = label
2644 .extras
2645 .get("fromCluster")
2646 .and_then(|v| v.as_str().map(|s| s.to_string()));
2647 let to_cluster = label
2648 .extras
2649 .get("toCluster")
2650 .and_then(|v| v.as_str().map(|s| s.to_string()));
2651 let points = label
2652 .points
2653 .iter()
2654 .map(|p| LayoutPoint { x: p.x, y: p.y })
2655 .collect::<Vec<_>>();
2656 let label_pos = match (label.x, label.y) {
2657 (Some(x), Some(y)) if label.width > 0.0 || label.height > 0.0 => {
2658 Some(LayoutLabel {
2659 x,
2660 y,
2661 width: label.width,
2662 height: label.height,
2663 })
2664 }
2665 _ => None,
2666 };
2667 (points, label_pos, false, from_cluster, to_cluster)
2668 };
2669
2670 if subgraph_ids.contains(e.from.as_str()) && e.id.ends_with("-cyclic-special-1") {
2674 from_cluster = Some(e.from.clone());
2675 }
2676 if subgraph_ids.contains(e.to.as_str()) && e.id.ends_with("-cyclic-special-2") {
2677 to_cluster = Some(e.to.clone());
2678 }
2679
2680 if dx.abs() > 1e-6 || dy.abs() > 1e-6 {
2681 for p in &mut points {
2682 p.x += dx;
2683 p.y += dy;
2684 }
2685 if !label_pos_already_shifted {
2686 if let Some(l) = label_pos.as_mut() {
2687 l.x += dx;
2688 l.y += dy;
2689 }
2690 }
2691 }
2692 out_edges.push(LayoutEdge {
2693 id: e.id.clone(),
2694 from: e.from.clone(),
2695 to: e.to.clone(),
2696 from_cluster,
2697 to_cluster,
2698 points,
2699 label: label_pos,
2700 start_label_left: None,
2701 start_label_right: None,
2702 end_label_left: None,
2703 end_label_right: None,
2704 start_marker: None,
2705 end_marker: None,
2706 stroke_dasharray: None,
2707 });
2708 }
2709
2710 let mut node_shape_by_id: HashMap<&str, &str> = HashMap::new();
2717 for n in &model.nodes {
2718 if let Some(s) = n.layout_shape.as_deref() {
2719 node_shape_by_id.insert(n.id.as_str(), s);
2720 }
2721 }
2722 let mut layout_node_by_id: HashMap<&str, &LayoutNode> = HashMap::new();
2723 for n in &out_nodes {
2724 layout_node_by_id.insert(n.id.as_str(), n);
2725 }
2726
2727 fn diamond_intersection(node: &LayoutNode, toward: &LayoutPoint) -> Option<LayoutPoint> {
2728 let vx = toward.x - node.x;
2729 let vy = toward.y - node.y;
2730 if !(vx.is_finite() && vy.is_finite()) {
2731 return None;
2732 }
2733 if vx.abs() <= 1e-12 && vy.abs() <= 1e-12 {
2734 return None;
2735 }
2736 let hw = (node.width / 2.0).max(1e-9);
2737 let hh = (node.height / 2.0).max(1e-9);
2738 let denom = vx.abs() / hw + vy.abs() / hh;
2739 if !(denom.is_finite() && denom > 0.0) {
2740 return None;
2741 }
2742 let t = 1.0 / denom;
2743 Some(LayoutPoint {
2744 x: node.x + vx * t,
2745 y: node.y + vy * t,
2746 })
2747 }
2748
2749 for e in &mut out_edges {
2750 if e.points.len() < 2 {
2751 continue;
2752 }
2753
2754 if let Some(node) = layout_node_by_id.get(e.from.as_str()) {
2755 if !node.is_cluster {
2756 let shape = node_shape_by_id
2757 .get(e.from.as_str())
2758 .copied()
2759 .unwrap_or("squareRect");
2760 if matches!(shape, "diamond" | "question" | "diam") {
2761 if let Some(p) = diamond_intersection(node, &e.points[1]) {
2762 e.points[0] = p;
2763 }
2764 }
2765 }
2766 }
2767 if let Some(node) = layout_node_by_id.get(e.to.as_str()) {
2768 if !node.is_cluster {
2769 let shape = node_shape_by_id
2770 .get(e.to.as_str())
2771 .copied()
2772 .unwrap_or("squareRect");
2773 if matches!(shape, "diamond" | "question" | "diam") {
2774 let n = e.points.len();
2775 if let Some(p) = diamond_intersection(node, &e.points[n - 2]) {
2776 e.points[n - 1] = p;
2777 }
2778 }
2779 }
2780 }
2781 }
2782
2783 let bounds = compute_bounds(&out_nodes, &out_edges);
2784
2785 if let Some(s) = build_output_start {
2786 timings.build_output = s.elapsed();
2787 }
2788 if let Some(s) = total_start {
2789 timings.total = s.elapsed();
2790 let dagre_overhead = timings
2791 .layout_recursive
2792 .checked_sub(timings.dagre_total)
2793 .unwrap_or_default();
2794 eprintln!(
2795 "[layout-timing] diagram=flowchart-v2 total={:?} deserialize={:?} expand_self_loops={:?} build_graph={:?} extract_clusters={:?} dom_order={:?} layout_recursive={:?} dagre_calls={} dagre_total={:?} dagre_overhead={:?} place_graph={:?} build_output={:?}",
2796 timings.total,
2797 timings.deserialize,
2798 timings.expand_self_loops,
2799 timings.build_graph,
2800 timings.extract_clusters,
2801 timings.dom_order,
2802 timings.layout_recursive,
2803 timings.dagre_calls,
2804 timings.dagre_total,
2805 dagre_overhead,
2806 timings.place_graph,
2807 timings.build_output,
2808 );
2809 }
2810
2811 Ok(FlowchartV2Layout {
2812 nodes: out_nodes,
2813 edges: out_edges,
2814 clusters,
2815 bounds,
2816 dom_node_order_by_root,
2817 })
2818}