@@ -1139,24 +1139,56 @@ def _align_symfan_section_to_row_feeder(graph: MetroGraph) -> None:
11391139 shift_section (graph , section , dy = delta )
11401140
11411141
1142+ def _entry_fork_join (
1143+ graph : MetroGraph , section : Section , branches : list [str ]
1144+ ) -> Station | None :
1145+ """The station a port-fed two-way fork cleanly reconverges at, or None.
1146+
1147+ The port-fork counterpart of :func:`_iter_fork_join_diamonds`, which admits
1148+ only station forks. Requiring the two branches to be the join's *only*
1149+ feeders keeps it the point where the whole bundle reunites, so no third
1150+ feeder has a competing claim on its Y.
1151+ """
1152+ if len (branches ) != 2 :
1153+ return None
1154+ succs = [_in_section_ontrack_successors (graph , section , b ) for b in branches ]
1155+ if succs [0 ] != succs [1 ] or len (succs [0 ]) != 1 :
1156+ return None
1157+ join_id = succs [0 ][0 ]
1158+ feeders = {
1159+ e .source
1160+ for e in graph .edges_to (join_id )
1161+ if _is_in_section_on_track (graph .station_for_edge_source (e ), section .id )
1162+ }
1163+ if feeders != set (branches ):
1164+ return None
1165+ return graph .stations [join_id ]
1166+
1167+
11421168def _center_lr_entry_ports_on_fork (graph : MetroGraph , y_spacing : float ) -> None :
1143- """Centre an LR entry port on the two-way fork it fans into .
1169+ """Centre an LR entry port, and the join it reconverges at, on its two-way fork.
11441170
11451171 Under ``diamond_style: symmetric`` a section's LR entry port that fans into
11461172 branches at exactly two distinct Ys should sit at their midpoint, so the
11471173 fork reads symmetric about the incoming bundle and the run from the feeding
11481174 section arrives straight. Otherwise the port stays pinned to whichever
11491175 branch the section layout seated it on (e.g. the top branch of a
1150- reconverging diamond), leaving the inter-section run kinked. Only the port
1151- moves; the branch stations keep their places.
1176+ reconverging diamond), leaving the inter-section run kinked.
1177+
1178+ When the fork reconverges at a clean in-section join, that join and its
1179+ linear trunk continuation ride the same midpoint centreline, so the diamond
1180+ closes symmetrically and the trunk leaves it straight. A station fork gets
1181+ this for free -- the join inherits the fork's own trunk track -- but a port
1182+ fork has no station on the centreline to inherit from, so the join would
1183+ otherwise stay on the track of whichever branch fed it first, turning the
1184+ diamond into a lopsided fan. The branch stations themselves keep their
1185+ places either way.
11521186
11531187 An off-grid midpoint (branches an odd number of slots apart) is only a
1154- valid seat when the fork reconverges at an in-section join: that join
1155- already sits at the midpoint, so the port matches it and the exit fork it
1156- mirrors. A non-reconverging dead-end fan instead keeps its port on the
1157- feeder trunk with the branches straddling half a slot either side, so
1158- seating the port on the off-grid midpoint there would drag the row's trunk
1159- off the inter-section run.
1188+ valid seat when the fork reconverges: a non-reconverging dead-end fan
1189+ instead keeps its port on the feeder trunk with the branches straddling
1190+ half a slot either side, so seating the port on the off-grid midpoint there
1191+ would drag the row's trunk off the inter-section run.
11601192 """
11611193 if graph .diamond_style != "symmetric" :
11621194 return
@@ -1181,10 +1213,17 @@ def _center_lr_entry_ports_on_fork(graph: MetroGraph, y_spacing: float) -> None:
11811213 midpoint = (branch_ys [0 ] + branch_ys [1 ]) / 2.0
11821214 if abs (graph .stations [pid ].y - midpoint ) >= 1.0 :
11831215 _set_port_y (graph , pid , midpoint )
1216+ join = _entry_fork_join (graph , section , branches )
1217+ if join is not None and abs (join .y - midpoint ) >= 1.0 :
1218+ join .y = midpoint
1219+ _pull_continuation_onto (graph , section , join )
11841220 if off_grid :
1185- # The port (and the diamond spine it feeds) rides the off-grid
1186- # midpoint centreline, like the join it reconverges at, so the
1187- # grid snap must treat it as half-grid, not re-seat it on a row.
1221+ # The port rides the off-grid midpoint centreline, so the grid
1222+ # snap must treat it as half-grid, not re-seat it on a row. The
1223+ # spine it feeds is deliberately left out: the snap runs earlier
1224+ # in the pipeline, so entries here would reach it only on a
1225+ # subsequent layout pass, where a whole spine of them would
1226+ # outvote the branch rows for the group's grid origin.
11881227 graph .half_grid_station_ids .add (pid )
11891228
11901229
0 commit comments