Skip to content

Commit 395f8f9

Browse files
committed
refactor: centralize flowchart lane planning
1 parent 4ec48e3 commit 395f8f9

2 files changed

Lines changed: 134 additions & 76 deletions

File tree

src/layout/flowchart/edge_pipeline.rs

Lines changed: 4 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -416,79 +416,10 @@ pub(in crate::layout) fn build_routed_edges(ctx: RoutedEdgeBuildContext<'_>) ->
416416
}
417417

418418
let edge_routing_start = Instant::now();
419-
let pair_counts = build_edge_pair_counts(&graph.edges);
420-
let mut pair_seen: HashMap<(String, String), usize> = HashMap::new();
421-
let mut pair_index: Vec<usize> = vec![0; graph.edges.len()];
422-
for (idx, edge) in graph.edges.iter().enumerate() {
423-
let key = edge_pair_key(edge);
424-
let seen = pair_seen.entry(key).or_insert(0usize);
425-
pair_index[idx] = *seen;
426-
*seen += 1;
427-
}
428-
429-
let mut cross_edge_offsets = vec![0.0f32; graph.edges.len()];
430-
if graph.kind == DiagramKind::Flowchart {
431-
let is_horizontal_layout = is_horizontal(graph.direction);
432-
let band_size = (config.node_spacing * 2.0).max(30.0);
433-
let mut groups: HashMap<i32, Vec<(usize, f32)>> = HashMap::new();
434-
for (idx, edge) in graph.edges.iter().enumerate() {
435-
let from_layout = nodes.get(&edge.from).expect("from node missing");
436-
let to_layout = nodes.get(&edge.to).expect("to node missing");
437-
let temp_from = from_layout.anchor_subgraph.and_then(|idx| {
438-
subgraphs
439-
.get(idx)
440-
.map(|sub| anchor_layout_for_edge(from_layout, sub, graph.direction, true))
441-
});
442-
let temp_to = to_layout.anchor_subgraph.and_then(|idx| {
443-
subgraphs
444-
.get(idx)
445-
.map(|sub| anchor_layout_for_edge(to_layout, sub, graph.direction, false))
446-
});
447-
let from = temp_from.as_ref().unwrap_or(from_layout);
448-
let to = temp_to.as_ref().unwrap_or(to_layout);
449-
let from_center = (from.x + from.width / 2.0, from.y + from.height / 2.0);
450-
let to_center = (to.x + to.width / 2.0, to.y + to.height / 2.0);
451-
let dx = to_center.0 - from_center.0;
452-
let dy = to_center.1 - from_center.1;
453-
let cross_axis = if is_horizontal_layout {
454-
dy.abs()
455-
} else {
456-
dx.abs()
457-
};
458-
let main_axis = if is_horizontal_layout {
459-
dx.abs()
460-
} else {
461-
dy.abs()
462-
};
463-
let is_secondary = edge.style == crate::ir::EdgeStyle::Dotted || edge.label.is_some();
464-
if !is_secondary || cross_axis <= main_axis * 1.2 {
465-
continue;
466-
}
467-
let band_coord = if is_horizontal_layout {
468-
(from_center.0 + to_center.0) * 0.5
469-
} else {
470-
(from_center.1 + to_center.1) * 0.5
471-
};
472-
let bucket = (band_coord / band_size).round() as i32;
473-
let sort_key = if is_horizontal_layout {
474-
(from_center.1 + to_center.1) * 0.5
475-
} else {
476-
(from_center.0 + to_center.0) * 0.5
477-
};
478-
groups.entry(bucket).or_default().push((idx, sort_key));
479-
}
480-
let spacing = (config.node_spacing * 0.45).max(8.0);
481-
for (_bucket, mut group) in groups {
482-
if group.len() <= 1 {
483-
continue;
484-
}
485-
group.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
486-
let center = (group.len() as f32 - 1.0) * 0.5;
487-
for (pos, (idx, _)) in group.iter().enumerate() {
488-
cross_edge_offsets[*idx] = (pos as f32 - center) * spacing;
489-
}
490-
}
491-
}
419+
let lane_assignments = plan::plan_edge_lanes(graph, nodes, subgraphs, config);
420+
let pair_counts = lane_assignments.pair_counts;
421+
let pair_index = lane_assignments.pair_index;
422+
let cross_edge_offsets = lane_assignments.cross_edge_offsets;
492423

493424
let mut route_order: Vec<(u8, f32, f32, usize)> = Vec::with_capacity(graph.edges.len());
494425
let dense_flowchart_routing = graph.kind == DiagramKind::Flowchart

src/layout/flowchart/plan.rs

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
//! changing output. Later phases can move bundle, route, and label decisions
88
//! into this model one stage at a time.
99
10+
use std::cmp::Ordering;
1011
use std::collections::{BTreeMap, HashMap};
1112

1213
use crate::config::LayoutConfig;
13-
use crate::ir::Graph;
14+
use crate::ir::{DiagramKind, Graph};
1415

1516
use super::super::routing::{
16-
EdgePortInfo, EdgeSide, anchor_point_for_node, edge_pair_key, port_stub_length,
17+
EdgePortInfo, EdgeSide, anchor_point_for_node, build_edge_pair_counts, edge_pair_key,
18+
is_horizontal, port_stub_length,
1719
};
1820
use super::super::{
1921
FLOWCHART_PORT_ROUTE_BIAS_MAX_RATIO, FLOWCHART_PORT_ROUTE_BIAS_RATIO, MULTI_EDGE_OFFSET_RATIO,
@@ -35,6 +37,13 @@ pub(super) struct FlowchartLayoutPlan {
3537
pub(super) labels: Vec<EdgeLabelPlan>,
3638
}
3739

40+
#[derive(Clone, Debug)]
41+
pub(super) struct EdgeLaneAssignments {
42+
pub(super) pair_counts: HashMap<(String, String), usize>,
43+
pub(super) pair_index: Vec<usize>,
44+
pub(super) cross_edge_offsets: Vec<f32>,
45+
}
46+
3847
#[derive(Clone, Debug)]
3948
pub(super) struct EdgePortPlan {
4049
pub(super) edge_idx: usize,
@@ -214,13 +223,100 @@ impl FlowchartLayoutPlan {
214223
}
215224
}
216225

226+
pub(super) fn plan_edge_lanes(
227+
graph: &Graph,
228+
nodes: &BTreeMap<String, NodeLayout>,
229+
subgraphs: &[SubgraphLayout],
230+
config: &LayoutConfig,
231+
) -> EdgeLaneAssignments {
232+
let pair_counts = build_edge_pair_counts(&graph.edges);
233+
let mut pair_seen: HashMap<(String, String), usize> = HashMap::new();
234+
let mut pair_index: Vec<usize> = vec![0; graph.edges.len()];
235+
for (idx, edge) in graph.edges.iter().enumerate() {
236+
let key = edge_pair_key(edge);
237+
let seen = pair_seen.entry(key).or_insert(0usize);
238+
pair_index[idx] = *seen;
239+
*seen += 1;
240+
}
241+
242+
let mut cross_edge_offsets = vec![0.0f32; graph.edges.len()];
243+
if graph.kind == DiagramKind::Flowchart {
244+
let is_horizontal_layout = is_horizontal(graph.direction);
245+
let band_size = (config.node_spacing * 2.0).max(30.0);
246+
let mut groups: HashMap<i32, Vec<(usize, f32)>> = HashMap::new();
247+
for (idx, edge) in graph.edges.iter().enumerate() {
248+
let from_layout = nodes.get(&edge.from).expect("from node missing");
249+
let to_layout = nodes.get(&edge.to).expect("to node missing");
250+
let temp_from = from_layout.anchor_subgraph.and_then(|anchor_idx| {
251+
subgraphs
252+
.get(anchor_idx)
253+
.map(|sub| anchor_layout_for_edge(from_layout, sub, graph.direction, true))
254+
});
255+
let temp_to = to_layout.anchor_subgraph.and_then(|anchor_idx| {
256+
subgraphs
257+
.get(anchor_idx)
258+
.map(|sub| anchor_layout_for_edge(to_layout, sub, graph.direction, false))
259+
});
260+
let from = temp_from.as_ref().unwrap_or(from_layout);
261+
let to = temp_to.as_ref().unwrap_or(to_layout);
262+
let from_center = (from.x + from.width / 2.0, from.y + from.height / 2.0);
263+
let to_center = (to.x + to.width / 2.0, to.y + to.height / 2.0);
264+
let dx = to_center.0 - from_center.0;
265+
let dy = to_center.1 - from_center.1;
266+
let cross_axis = if is_horizontal_layout {
267+
dy.abs()
268+
} else {
269+
dx.abs()
270+
};
271+
let main_axis = if is_horizontal_layout {
272+
dx.abs()
273+
} else {
274+
dy.abs()
275+
};
276+
let is_secondary = edge.style == crate::ir::EdgeStyle::Dotted || edge.label.is_some();
277+
if !is_secondary || cross_axis <= main_axis * 1.2 {
278+
continue;
279+
}
280+
let band_coord = if is_horizontal_layout {
281+
(from_center.0 + to_center.0) * 0.5
282+
} else {
283+
(from_center.1 + to_center.1) * 0.5
284+
};
285+
let bucket = (band_coord / band_size).round() as i32;
286+
let sort_key = if is_horizontal_layout {
287+
(from_center.1 + to_center.1) * 0.5
288+
} else {
289+
(from_center.0 + to_center.0) * 0.5
290+
};
291+
groups.entry(bucket).or_default().push((idx, sort_key));
292+
}
293+
let spacing = (config.node_spacing * 0.45).max(8.0);
294+
for (_bucket, mut group) in groups {
295+
if group.len() <= 1 {
296+
continue;
297+
}
298+
group.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
299+
let center = (group.len() as f32 - 1.0) * 0.5;
300+
for (pos, (idx, _)) in group.iter().enumerate() {
301+
cross_edge_offsets[*idx] = (pos as f32 - center) * spacing;
302+
}
303+
}
304+
}
305+
306+
EdgeLaneAssignments {
307+
pair_counts,
308+
pair_index,
309+
cross_edge_offsets,
310+
}
311+
}
312+
217313
#[cfg(test)]
218314
mod tests {
219315
use std::collections::{BTreeMap, HashMap};
220316

221317
use crate::config::LayoutConfig;
222318
use crate::ir::{Direction, Edge, EdgeStyle, Graph, NodeShape};
223-
use crate::layout::flowchart::plan::FlowchartLayoutPlan;
319+
use crate::layout::flowchart::plan::{FlowchartLayoutPlan, plan_edge_lanes};
224320
use crate::layout::routing::{EdgePortInfo, EdgeSide};
225321
use crate::layout::{NodeLayout, TextBlock};
226322

@@ -318,4 +414,35 @@ mod tests {
318414
plan.bundles[0].lanes[0].effective_offset < plan.bundles[0].lanes[1].effective_offset
319415
);
320416
}
417+
418+
#[test]
419+
fn lane_planner_assigns_stable_pair_indices() {
420+
let mut graph = Graph::new();
421+
graph.direction = Direction::LeftRight;
422+
graph.edges.push(edge("A", "B"));
423+
graph.edges.push(edge("B", "A"));
424+
graph.edges.push(edge("A", "C"));
425+
426+
let mut nodes = BTreeMap::new();
427+
nodes.insert("A".to_string(), node("A", 0.0, 0.0));
428+
nodes.insert("B".to_string(), node("B", 160.0, 0.0));
429+
nodes.insert("C".to_string(), node("C", 320.0, 0.0));
430+
431+
let assignments = plan_edge_lanes(&graph, &nodes, &[], &LayoutConfig::default());
432+
433+
assert_eq!(
434+
assignments
435+
.pair_counts
436+
.get(&("A".to_string(), "B".to_string())),
437+
Some(&2)
438+
);
439+
assert_eq!(
440+
assignments
441+
.pair_counts
442+
.get(&("A".to_string(), "C".to_string())),
443+
Some(&1)
444+
);
445+
assert_eq!(assignments.pair_index, vec![0, 1, 0]);
446+
assert_eq!(assignments.cross_edge_offsets, vec![0.0, 0.0, 0.0]);
447+
}
321448
}

0 commit comments

Comments
 (0)