Problem
Routing (layout/routing/core.py) emits one RoutedPath per graph edge (RoutedPath.line_id, RoutedPath.points — layout/routing/common.py:734-739), not one per metro line. render/svg.py:_render_edges (svg.py:3254-3330) then draws one <path>/<draw.Line> per RoutedPath, so a single line that is logically one continuous route gets painted as several adjacent or overlapping path segments rather than one stroke.
This already causes real double-paint artifacts:
layout/CONTRACT.md:2172-2175 documents "three doubled strokes over 40-60px, each overlapping a neighbouring line's lane."
docs/dev/routing.mdx:224-229 documents a fused-cotravelling case where "two distinct lines paint a single two-tone stripe."
Both have historically been patched as routing/column-seating bugs rather than fixed at the paint layer, which means the same class of bug can keep resurfacing under new topologies.
What already exists
The phase separation itself is sound and doesn't need to change:
- Routing produces pure geometry with no drawing/color decisions in it.
render/svg.py is already the sole place that resolves line color, stroke, and z-order (line_priority sort, svg.py:2255-2260).
There is also a partial precedent for deduping overlapping paint: on convergent fan-in, _classify_merge_edges (context.py:280) designates one predecessor's edge as the "trunk" carrying the shared segment and truncates the others to "branches"; emit_edge (core.py:409-463) explicitly skips edges covered by another via ctx.skip_edges / covering_edge_for_edge (convergences.py:187-189). Divergent fan-out has no equivalent — each diverging edge still emits its own full RoutedPath back through the shared initial run. The existing coincidence passes (_coincide_same_line_tracks, _coincide_fanout_opening_descents, _bundle_divergent_distinct_traverses, core.py:553-578) only align coordinates so overlapping strokes visually register as one stripe — they don't dedupe the underlying geometry.
Proposal
Add a consolidation step between the existing polyline/bridge computation and _render_edges (slots in around svg.py:2241-2274) that:
- Groups
RoutedPaths by line_id.
- Extends the convergence "covering edge" idea to divergence, so a shared segment is only ever emitted once regardless of fan-out/fan-in.
- Emits one path per line per contiguous run for
_render_edges to draw.
This should let the known doubled-stroke/fused-stripe cases be fixed generically instead of one topology at a time.
Main risk / open question
When two different lines' segments coincide, the merge needs to preserve correct z-order/priority for the segment that does get drawn (currently line_priority is resolved per-RoutedPath, so merging paths from different lines needs an explicit tie-break, not just first-wins). Needs design before implementation.
Problem
Routing (
layout/routing/core.py) emits oneRoutedPathper graph edge (RoutedPath.line_id,RoutedPath.points—layout/routing/common.py:734-739), not one per metro line.render/svg.py:_render_edges(svg.py:3254-3330) then draws one<path>/<draw.Line>perRoutedPath, so a single line that is logically one continuous route gets painted as several adjacent or overlapping path segments rather than one stroke.This already causes real double-paint artifacts:
layout/CONTRACT.md:2172-2175documents "three doubled strokes over 40-60px, each overlapping a neighbouring line's lane."docs/dev/routing.mdx:224-229documents a fused-cotravelling case where "two distinct lines paint a single two-tone stripe."Both have historically been patched as routing/column-seating bugs rather than fixed at the paint layer, which means the same class of bug can keep resurfacing under new topologies.
What already exists
The phase separation itself is sound and doesn't need to change:
render/svg.pyis already the sole place that resolves line color, stroke, and z-order (line_prioritysort,svg.py:2255-2260).There is also a partial precedent for deduping overlapping paint: on convergent fan-in,
_classify_merge_edges(context.py:280) designates one predecessor's edge as the "trunk" carrying the shared segment and truncates the others to "branches";emit_edge(core.py:409-463) explicitly skips edges covered by another viactx.skip_edges/covering_edge_for_edge(convergences.py:187-189). Divergent fan-out has no equivalent — each diverging edge still emits its own fullRoutedPathback through the shared initial run. The existing coincidence passes (_coincide_same_line_tracks,_coincide_fanout_opening_descents,_bundle_divergent_distinct_traverses,core.py:553-578) only align coordinates so overlapping strokes visually register as one stripe — they don't dedupe the underlying geometry.Proposal
Add a consolidation step between the existing polyline/bridge computation and
_render_edges(slots in aroundsvg.py:2241-2274) that:RoutedPaths byline_id._render_edgesto draw.This should let the known doubled-stroke/fused-stripe cases be fixed generically instead of one topology at a time.
Main risk / open question
When two different lines' segments coincide, the merge needs to preserve correct z-order/priority for the segment that does get drawn (currently
line_priorityis resolved per-RoutedPath, so merging paths from different lines needs an explicit tie-break, not just first-wins). Needs design before implementation.