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