You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A section's flow-aligned exit port fed by exactly two in-section stations that feed nothing else is a two-way join: the two branches reunite at the port. Stage 3.1 seats such a port at its feeders' midpoint, which is correct. Stage 4.1 _align_ports_to_downstream then overwrites it in the diamond_style == "straight" branch, replacing the midpoint with the downstream section's dominant target Y and never reading the exit section's own feeders. The port lands outside its feeders' span, so both legs leave the join turning the same way to reach it.
In all three the port sits above both feeders, not between them.
Mechanism
src/nf_metro/layout/phases/ports.py:692-695, applied via _set_port_y at ports.py:733:
ifgraph.diamond_style=="straight":
# Snap to the Y that the most lines target, so the majority# of lines flow straight. Ties broken by topmost (smallest Y).target_y=dominant_value(downstream_ys)
downstream_ys comes from the entry port's targets in the next section (ports.py:685-690). Nothing consults the exit section's own feeders, so a two-way join's midpoint is discarded unconditionally.
Traced per fixture, watching the port's offset from each feeder rather than its absolute Y:
The same 3.1-correct / 4.1-broken pattern holds for the other two. The stage that sets the final raw number differs per fixture (6.13, 5.4, and 4.1 respectively), but that is incidental: the offset is frozen at 4.1, and every later stage rigid-translates the port together with its feeders.
Stages 5.4 and 6.13 are not implicated._tighten_lower_rows_after_shrink (bbox.py:1228-1242) and _compact_row_content_to_bbox_top (row_align.py:889-896) shift whole rows uniformly and move ports as cargo to keep them with their boxes; they have no interaction with this invariant. Stage 6.16's scoping of port re-alignment to vertical-flow sections is likewise not the obstacle.
Suggested direction
In the straight branch, when the exit port is a two-way join, either skip the downstream-alignment override or fold the feeders' midpoint into the target rather than taking dominant_value(downstream_ys) outright.
Only a port outside its feeders' span needs moving. A port already straddled by its feeders normally sits on a flat inter-section run, and nudging it to the exact midpoint trades that flat run for a step down onto the port and a step back up off it.
This is the same invariant PR fix(layout): seat a two-way join's exit port on its branches' centreline #1579 established for diamond_style: symmetric via _center_lr_exit_ports_on_join, tested by test_symmetric_exit_port_join_stays_within_its_feeders_span. Fixing the straight branch is what lets that test drop its style restriction and lets the invariant become a live guard, which today would abort these three fixtures.
Acceptance
The three fixtures above seat their exit port within its feeders' span.
The invariant test covers diamond_style: straight as well as symmetric.
Gallery render-diff reviewed for all three, which are shipped maps.
Summary
A section's flow-aligned exit port fed by exactly two in-section stations that feed nothing else is a two-way join: the two branches reunite at the port. Stage 3.1 seats such a port at its feeders' midpoint, which is correct. Stage 4.1
_align_ports_to_downstreamthen overwrites it in thediamond_style == "straight"branch, replacing the midpoint with the downstream section's dominant target Y and never reading the exit section's own feeders. The port lands outside its feeders' span, so both legs leave the join turning the same way to reach it.Three shipped maps carry this today.
Affected
examples/longread_variant_calling.mmdannotation__exit_left_6examples/topologies/convergent_offrow_exit_climb.mmdannotation__exit_right_6examples/variantprioritization.mmdformat_files__exit_right_2In all three the port sits above both feeders, not between them.
Mechanism
src/nf_metro/layout/phases/ports.py:692-695, applied via_set_port_yatports.py:733:downstream_yscomes from the entry port's targets in the next section (ports.py:685-690). Nothing consults the exit section's own feeders, so a two-way join's midpoint is discarded unconditionally.Traced per fixture, watching the port's offset from each feeder rather than its absolute Y:
The same 3.1-correct / 4.1-broken pattern holds for the other two. The stage that sets the final raw number differs per fixture (6.13, 5.4, and 4.1 respectively), but that is incidental: the offset is frozen at 4.1, and every later stage rigid-translates the port together with its feeders.
Stages 5.4 and 6.13 are not implicated.
_tighten_lower_rows_after_shrink(bbox.py:1228-1242) and_compact_row_content_to_bbox_top(row_align.py:889-896) shift whole rows uniformly and move ports as cargo to keep them with their boxes; they have no interaction with this invariant. Stage 6.16's scoping of port re-alignment to vertical-flow sections is likewise not the obstacle.Suggested direction
In the
straightbranch, when the exit port is a two-way join, either skip the downstream-alignment override or fold the feeders' midpoint into the target rather than takingdominant_value(downstream_ys)outright.Two constraints from the sibling work in #1573:
diamond_style: symmetricvia_center_lr_exit_ports_on_join, tested bytest_symmetric_exit_port_join_stays_within_its_feeders_span. Fixing thestraightbranch is what lets that test drop its style restriction and lets the invariant become a live guard, which today would abort these three fixtures.Acceptance
The three fixtures above seat their exit port within its feeders' span.
The invariant test covers
diamond_style: straightas well assymmetric.Gallery render-diff reviewed for all three, which are shipped maps.
Version: nf-metro 1.1.0 @ 32d4f17