--- a/src/engines/graph/algorithms/layered/kernel/graph.rs +++ b/src/engines/graph/algorithms/layered/kernel/graph.rs @@ -358,6 +358,14 @@ compound_nodes.insert(parent_idx); } } + // A titled subgraph with no member nodes is nobody's parent, so the + // parent map above never registers it as a compound. It still lays out + // as an (empty) subgraph box: border construction, title insertion, and + // rank-span extraction all read it like a subgraph. Mermaid draws the + // titled cluster, so keep it here; compound_titles must stay a subset. + for &titled_idx in &compound_titles { + compound_nodes.insert(titled_idx); + } let model_order: Vec> = (0..n).map(Some).collect(); let mut compound_semantic_order = vec![None; n]; --- a/src/engines/graph/algorithms/layered/kernel/regression_tests.rs +++ b/src/engines/graph/algorithms/layered/kernel/regression_tests.rs @@ -1180,6 +1180,42 @@ } #[test] +fn test_layout_titled_empty_subgraph_end_to_end() { + // Mermaid renders `subgraph e ["label"] end` as an empty cluster box. The + // graph model keeps it as a titled node with no children; it is nobody's + // parent, so the parent map alone never registers it as a compound, and + // title insertion could not index a border_top for it (regression: the + // HashMap index panicked with "no entry found for key"). + let mut graph: DiGraph<(f64, f64)> = DiGraph::new(); + graph.add_node("sg1", (0.0, 0.0)); + graph.add_node("empty_sg", (0.0, 0.0)); + graph.add_node("A", (40.0, 20.0)); + graph.add_node("B", (40.0, 20.0)); + graph.add_edge("A", "B"); + graph.set_parent("A", "sg1"); + graph.set_parent("B", "sg1"); + graph.set_has_title("sg1"); + graph.set_has_title("empty_sg"); + + let lg = LayoutGraph::from_digraph(&graph, |_, dims| *dims); + for &titled in &lg.compound_titles { + assert!( + lg.compound_nodes.contains(&titled), + "compound_titles must stay a subset of compound_nodes" + ); + } + + let config = LayoutConfig::default(); + let result = layout(&graph, &config, |_, dims| *dims); + + assert!(result.nodes.contains_key(&"A".into())); + assert!(result.nodes.contains_key(&"B".into())); + assert!(result.subgraph_bounds.contains_key("sg1")); + // The empty compound has no member ranks; it contributes no bounds. + assert!(!result.subgraph_bounds.contains_key("empty_sg")); +} + +#[test] fn test_layout_multi_level_compound_nesting() { let mut g: DiGraph<(f64, f64)> = DiGraph::new(); g.add_node("A", (40.0, 20.0));