|
| 1 | +#![allow(dead_code)] |
| 2 | + |
| 3 | +//! Internal flowchart layout plan model. |
| 4 | +//! |
| 5 | +//! This module is the migration seam for the flowchart layout redesign. The |
| 6 | +//! first version mirrors facts already produced by the existing pipeline without |
| 7 | +//! changing output. Later phases can move bundle, route, and label decisions |
| 8 | +//! into this model one stage at a time. |
| 9 | +
|
| 10 | +use std::collections::{BTreeMap, HashMap}; |
| 11 | + |
| 12 | +use crate::config::LayoutConfig; |
| 13 | +use crate::ir::Graph; |
| 14 | + |
| 15 | +use super::super::routing::{ |
| 16 | + EdgePortInfo, EdgeSide, anchor_point_for_node, edge_pair_key, port_stub_length, |
| 17 | +}; |
| 18 | +use super::super::{ |
| 19 | + FLOWCHART_PORT_ROUTE_BIAS_MAX_RATIO, FLOWCHART_PORT_ROUTE_BIAS_RATIO, MULTI_EDGE_OFFSET_RATIO, |
| 20 | + NodeLayout, SubgraphLayout, TextBlock, anchor_layout_for_edge, |
| 21 | +}; |
| 22 | + |
| 23 | +#[derive(Clone, Debug, PartialEq, Eq, Hash)] |
| 24 | +pub(super) struct BundleKey { |
| 25 | + pub(super) a: String, |
| 26 | + pub(super) b: String, |
| 27 | +} |
| 28 | + |
| 29 | +#[derive(Clone, Debug)] |
| 30 | +pub(super) struct FlowchartLayoutPlan { |
| 31 | + pub(super) edge_count: usize, |
| 32 | + pub(super) ports: Vec<EdgePortPlan>, |
| 33 | + pub(super) bundles: Vec<EdgeBundlePlan>, |
| 34 | + pub(super) routes: Vec<EdgeRoutePlan>, |
| 35 | + pub(super) labels: Vec<EdgeLabelPlan>, |
| 36 | +} |
| 37 | + |
| 38 | +#[derive(Clone, Debug)] |
| 39 | +pub(super) struct EdgePortPlan { |
| 40 | + pub(super) edge_idx: usize, |
| 41 | + pub(super) from: String, |
| 42 | + pub(super) to: String, |
| 43 | + pub(super) start_side: EdgeSide, |
| 44 | + pub(super) end_side: EdgeSide, |
| 45 | + pub(super) start_offset: f32, |
| 46 | + pub(super) end_offset: f32, |
| 47 | + pub(super) start_point: (f32, f32), |
| 48 | + pub(super) end_point: (f32, f32), |
| 49 | + pub(super) stub_len: f32, |
| 50 | +} |
| 51 | + |
| 52 | +#[derive(Clone, Debug)] |
| 53 | +pub(super) struct EdgeBundlePlan { |
| 54 | + pub(super) key: BundleKey, |
| 55 | + pub(super) lanes: Vec<EdgeLanePlan>, |
| 56 | +} |
| 57 | + |
| 58 | +#[derive(Clone, Debug)] |
| 59 | +pub(super) struct EdgeLanePlan { |
| 60 | + pub(super) edge_idx: usize, |
| 61 | + pub(super) lane_index: usize, |
| 62 | + pub(super) lane_count: usize, |
| 63 | + pub(super) base_offset: f32, |
| 64 | + pub(super) cross_edge_offset: f32, |
| 65 | + pub(super) effective_offset: f32, |
| 66 | +} |
| 67 | + |
| 68 | +#[derive(Clone, Debug)] |
| 69 | +pub(super) struct EdgeRoutePlan { |
| 70 | + pub(super) edge_idx: usize, |
| 71 | + pub(super) points: Vec<(f32, f32)>, |
| 72 | + pub(super) approach_start: EdgeSide, |
| 73 | + pub(super) approach_end: EdgeSide, |
| 74 | +} |
| 75 | + |
| 76 | +#[derive(Clone, Debug)] |
| 77 | +pub(super) struct EdgeLabelPlan { |
| 78 | + pub(super) edge_idx: usize, |
| 79 | + pub(super) label: Option<TextBlock>, |
| 80 | + pub(super) anchor: Option<(f32, f32)>, |
| 81 | + pub(super) reserved_center: Option<(f32, f32)>, |
| 82 | +} |
| 83 | + |
| 84 | +impl FlowchartLayoutPlan { |
| 85 | + #[allow(clippy::too_many_arguments)] |
| 86 | + pub(super) fn from_current_pipeline( |
| 87 | + graph: &Graph, |
| 88 | + nodes: &BTreeMap<String, NodeLayout>, |
| 89 | + subgraphs: &[SubgraphLayout], |
| 90 | + edge_ports: &[EdgePortInfo], |
| 91 | + pair_counts: &HashMap<(String, String), usize>, |
| 92 | + pair_index: &[usize], |
| 93 | + cross_edge_offsets: &[f32], |
| 94 | + routed_points: &[Vec<(f32, f32)>], |
| 95 | + label_anchors: &[Option<(f32, f32)>], |
| 96 | + route_label_centers: &[Option<(f32, f32)>], |
| 97 | + edge_route_labels: &[Option<TextBlock>], |
| 98 | + config: &LayoutConfig, |
| 99 | + ) -> Self { |
| 100 | + let mut ports = Vec::with_capacity(graph.edges.len()); |
| 101 | + let mut bundle_lanes: HashMap<BundleKey, Vec<EdgeLanePlan>> = HashMap::new(); |
| 102 | + let mut routes = Vec::with_capacity(graph.edges.len()); |
| 103 | + let mut labels = Vec::with_capacity(graph.edges.len()); |
| 104 | + |
| 105 | + for (idx, edge) in graph.edges.iter().enumerate() { |
| 106 | + let Some(port_info) = edge_ports.get(idx).copied() else { |
| 107 | + continue; |
| 108 | + }; |
| 109 | + let from_layout = nodes.get(&edge.from).expect("from node missing"); |
| 110 | + let to_layout = nodes.get(&edge.to).expect("to node missing"); |
| 111 | + let temp_from = from_layout.anchor_subgraph.and_then(|anchor_idx| { |
| 112 | + subgraphs |
| 113 | + .get(anchor_idx) |
| 114 | + .map(|sub| anchor_layout_for_edge(from_layout, sub, graph.direction, true)) |
| 115 | + }); |
| 116 | + let temp_to = to_layout.anchor_subgraph.and_then(|anchor_idx| { |
| 117 | + subgraphs |
| 118 | + .get(anchor_idx) |
| 119 | + .map(|sub| anchor_layout_for_edge(to_layout, sub, graph.direction, false)) |
| 120 | + }); |
| 121 | + let from = temp_from.as_ref().unwrap_or(from_layout); |
| 122 | + let to = temp_to.as_ref().unwrap_or(to_layout); |
| 123 | + let start_point = |
| 124 | + anchor_point_for_node(from, port_info.start_side, port_info.start_offset); |
| 125 | + let end_point = anchor_point_for_node(to, port_info.end_side, port_info.end_offset); |
| 126 | + let stub_len = port_stub_length(config, from, to); |
| 127 | + |
| 128 | + ports.push(EdgePortPlan { |
| 129 | + edge_idx: idx, |
| 130 | + from: edge.from.clone(), |
| 131 | + to: edge.to.clone(), |
| 132 | + start_side: port_info.start_side, |
| 133 | + end_side: port_info.end_side, |
| 134 | + start_offset: port_info.start_offset, |
| 135 | + end_offset: port_info.end_offset, |
| 136 | + start_point, |
| 137 | + end_point, |
| 138 | + stub_len, |
| 139 | + }); |
| 140 | + |
| 141 | + let pair = edge_pair_key(edge); |
| 142 | + let total = *pair_counts.get(&pair).unwrap_or(&1); |
| 143 | + let lane_index = pair_index.get(idx).copied().unwrap_or_default(); |
| 144 | + let base_offset = if total > 1 { |
| 145 | + (lane_index as f32 - (total as f32 - 1.0) / 2.0) |
| 146 | + * (config.node_spacing * MULTI_EDGE_OFFSET_RATIO) |
| 147 | + } else { |
| 148 | + 0.0 |
| 149 | + }; |
| 150 | + let cross_edge_offset = cross_edge_offsets.get(idx).copied().unwrap_or_default(); |
| 151 | + let raw_bias = |
| 152 | + (port_info.start_offset - port_info.end_offset) * FLOWCHART_PORT_ROUTE_BIAS_RATIO; |
| 153 | + let max_bias = (config.node_spacing * FLOWCHART_PORT_ROUTE_BIAS_MAX_RATIO).max(8.0); |
| 154 | + let effective_offset = |
| 155 | + base_offset + cross_edge_offset + raw_bias.clamp(-max_bias, max_bias); |
| 156 | + bundle_lanes |
| 157 | + .entry(BundleKey { |
| 158 | + a: pair.0, |
| 159 | + b: pair.1, |
| 160 | + }) |
| 161 | + .or_default() |
| 162 | + .push(EdgeLanePlan { |
| 163 | + edge_idx: idx, |
| 164 | + lane_index, |
| 165 | + lane_count: total, |
| 166 | + base_offset, |
| 167 | + cross_edge_offset, |
| 168 | + effective_offset, |
| 169 | + }); |
| 170 | + |
| 171 | + routes.push(EdgeRoutePlan { |
| 172 | + edge_idx: idx, |
| 173 | + points: routed_points.get(idx).cloned().unwrap_or_default(), |
| 174 | + approach_start: port_info.start_side, |
| 175 | + approach_end: port_info.end_side, |
| 176 | + }); |
| 177 | + |
| 178 | + labels.push(EdgeLabelPlan { |
| 179 | + edge_idx: idx, |
| 180 | + label: edge_route_labels.get(idx).cloned().unwrap_or_default(), |
| 181 | + anchor: label_anchors.get(idx).copied().unwrap_or_default(), |
| 182 | + reserved_center: route_label_centers.get(idx).copied().unwrap_or_default(), |
| 183 | + }); |
| 184 | + } |
| 185 | + |
| 186 | + let mut bundles: Vec<EdgeBundlePlan> = bundle_lanes |
| 187 | + .into_iter() |
| 188 | + .map(|(key, mut lanes)| { |
| 189 | + lanes.sort_by_key(|lane| lane.edge_idx); |
| 190 | + EdgeBundlePlan { key, lanes } |
| 191 | + }) |
| 192 | + .collect(); |
| 193 | + bundles.sort_by(|a, b| a.key.a.cmp(&b.key.a).then_with(|| a.key.b.cmp(&b.key.b))); |
| 194 | + |
| 195 | + Self { |
| 196 | + edge_count: graph.edges.len(), |
| 197 | + ports, |
| 198 | + bundles, |
| 199 | + routes, |
| 200 | + labels, |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + pub(super) fn is_consistent(&self) -> bool { |
| 205 | + self.ports.len() == self.edge_count |
| 206 | + && self.routes.len() == self.edge_count |
| 207 | + && self.labels.len() == self.edge_count |
| 208 | + && self |
| 209 | + .bundles |
| 210 | + .iter() |
| 211 | + .flat_map(|bundle| bundle.lanes.iter()) |
| 212 | + .count() |
| 213 | + == self.edge_count |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +#[cfg(test)] |
| 218 | +mod tests { |
| 219 | + use std::collections::{BTreeMap, HashMap}; |
| 220 | + |
| 221 | + use crate::config::LayoutConfig; |
| 222 | + use crate::ir::{Direction, Edge, EdgeStyle, Graph, NodeShape}; |
| 223 | + use crate::layout::flowchart::plan::FlowchartLayoutPlan; |
| 224 | + use crate::layout::routing::{EdgePortInfo, EdgeSide}; |
| 225 | + use crate::layout::{NodeLayout, TextBlock}; |
| 226 | + |
| 227 | + fn node(id: &str, x: f32, y: f32) -> NodeLayout { |
| 228 | + NodeLayout { |
| 229 | + id: id.to_string(), |
| 230 | + x, |
| 231 | + y, |
| 232 | + width: 80.0, |
| 233 | + height: 40.0, |
| 234 | + label: TextBlock { |
| 235 | + lines: vec![id.to_string()], |
| 236 | + width: 10.0, |
| 237 | + height: 10.0, |
| 238 | + }, |
| 239 | + shape: NodeShape::Rectangle, |
| 240 | + style: Default::default(), |
| 241 | + link: None, |
| 242 | + anchor_subgraph: None, |
| 243 | + hidden: false, |
| 244 | + icon: None, |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + fn edge(from: &str, to: &str) -> Edge { |
| 249 | + Edge { |
| 250 | + from: from.to_string(), |
| 251 | + to: to.to_string(), |
| 252 | + label: Some("label".to_string()), |
| 253 | + start_label: None, |
| 254 | + end_label: None, |
| 255 | + directed: true, |
| 256 | + arrow_start: false, |
| 257 | + arrow_end: true, |
| 258 | + arrow_start_kind: None, |
| 259 | + arrow_end_kind: None, |
| 260 | + start_decoration: None, |
| 261 | + end_decoration: None, |
| 262 | + style: EdgeStyle::Solid, |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + #[test] |
| 267 | + fn plan_groups_parallel_edges_into_bundle_lanes() { |
| 268 | + let mut graph = Graph::new(); |
| 269 | + graph.direction = Direction::LeftRight; |
| 270 | + graph.edges.push(edge("A", "B")); |
| 271 | + graph.edges.push(edge("A", "B")); |
| 272 | + |
| 273 | + let mut nodes = BTreeMap::new(); |
| 274 | + nodes.insert("A".to_string(), node("A", 0.0, 0.0)); |
| 275 | + nodes.insert("B".to_string(), node("B", 160.0, 0.0)); |
| 276 | + |
| 277 | + let ports = vec![ |
| 278 | + EdgePortInfo { |
| 279 | + start_side: EdgeSide::Right, |
| 280 | + end_side: EdgeSide::Left, |
| 281 | + start_offset: -8.0, |
| 282 | + end_offset: -8.0, |
| 283 | + }, |
| 284 | + EdgePortInfo { |
| 285 | + start_side: EdgeSide::Right, |
| 286 | + end_side: EdgeSide::Left, |
| 287 | + start_offset: 8.0, |
| 288 | + end_offset: 8.0, |
| 289 | + }, |
| 290 | + ]; |
| 291 | + let mut pair_counts = HashMap::new(); |
| 292 | + pair_counts.insert(("A".to_string(), "B".to_string()), 2); |
| 293 | + let pair_index = vec![0, 1]; |
| 294 | + let routed_points = vec![ |
| 295 | + vec![(80.0, 12.0), (160.0, 12.0)], |
| 296 | + vec![(80.0, 28.0), (160.0, 28.0)], |
| 297 | + ]; |
| 298 | + let labels = vec![None, None]; |
| 299 | + let plan = FlowchartLayoutPlan::from_current_pipeline( |
| 300 | + &graph, |
| 301 | + &nodes, |
| 302 | + &[], |
| 303 | + &ports, |
| 304 | + &pair_counts, |
| 305 | + &pair_index, |
| 306 | + &[0.0, 0.0], |
| 307 | + &routed_points, |
| 308 | + &[Some((120.0, 12.0)), Some((120.0, 28.0))], |
| 309 | + &[None, None], |
| 310 | + &labels, |
| 311 | + &LayoutConfig::default(), |
| 312 | + ); |
| 313 | + |
| 314 | + assert!(plan.is_consistent()); |
| 315 | + assert_eq!(plan.bundles.len(), 1); |
| 316 | + assert_eq!(plan.bundles[0].lanes.len(), 2); |
| 317 | + assert!( |
| 318 | + plan.bundles[0].lanes[0].effective_offset < plan.bundles[0].lanes[1].effective_offset |
| 319 | + ); |
| 320 | + } |
| 321 | +} |
0 commit comments