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