Skip to content

Consolidate per-line RoutedPaths into a single stroke before painting #1823

Description

@pinin4fjords

Problem

Routing (layout/routing/core.py) emits one RoutedPath per graph edge (RoutedPath.line_id, RoutedPath.pointslayout/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:

  1. Groups RoutedPaths by line_id.
  2. Extends the convergence "covering edge" idea to divergence, so a shared segment is only ever emitted once regardless of fan-out/fan-in.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions