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 ;
1011use std:: collections:: { BTreeMap , HashMap } ;
1112
1213use crate :: config:: LayoutConfig ;
13- use crate :: ir:: Graph ;
14+ use crate :: ir:: { DiagramKind , Graph } ;
1415
1516use 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} ;
1820use 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 ) ]
3948pub ( 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) ]
218314mod 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