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