Skip to content

Draw an occupied block as a flat red wash, not a hatch - #183

Merged
bazauto merged 1 commit into
mainfrom
feat/flat-occupied-wash
Aug 27, 2026
Merged

bazauto merged 1 commit into
mainfrom
feat/flat-occupied-wash

Conversation

@bazauto

@bazauto bazauto commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Operator feedback on the control view: "the hashing stands out a bit too much" on an
occupied block, and hashing on unknown is fine because that reads as a fault rather than
an operational state. That is the right instinct, and the diagram's own reasoning agrees
with it.

Texture now marks a fault state, not an operational one

A hatch is the loudest non-colour mark the diagram has, and docs/diagram-encoding.md D2
already said unknown should be the most visually assertive of the three occupancy states,
because it is the one that refuses routes. Spending a hatch on occupied — the state a
working railway sits in most of the time — put the busiest texture on the ordinary case and
left the fail-safe state shouting over it. D2's claim about unknown was half true at best
while the two were competing for the same channel.

So occupied is a flat red wash, clear stays a flat green wash, and unknown keeps
its cross-hatch and is now the only hatch on the diagram. The diag-occupied pattern and
its <pattern> definition are deleted. New rule, recorded as D10: texture separates a
fault from an operational state, never one operational state from another
— which is also
the line #71's decorative track and #82's staleness will be measured against.

What the hatch was carrying, and what carries it now

#81's rule is not rhetorical for this pair. Composited over the #1e1e2e tile at the same
opacity, #f38ba8 and #a6e3a1 land 6.9 apart in RGB under simulated deuteranopia —
occupied and clear become the same colour for roughly 8% of men, which is exactly the
failure the whole document exists to prevent. Two flat washes separated only by hue would
have been the bug, not the fix.

So the flat wash carries its own non-colour distinction. OCCUPANCY_WASH_OPACITY draws
occupied at 0.55 and clear/unknown at the ordinary BLOCK_TINT_OPACITY (0.26):

occupied clear
composited wash #935a71 #41514c
luminance contrast, occ:clr 1.57:1
RGB distance, normal vision 52.1
RGB distance, deuteranopia 34.8 (was 6.9)
RGB distance, protanopia 22.3

With colour removed entirely, occupied is the heavier block — the right direction, since
it is also the one carrying a train. The run label's OCCUPANCY[…].glyph (■ □ ?) and the
legend on the status strip are unchanged, so weight is the second carrier, not the only one.
The resting layout is pixel-identical: only occupied moved.

Refused: a deeper, more saturated red

Tried and rejected. #e64553 and #ff6b6b both read as more emphatically red to normal
vision and worse to everyone else — #ff6b6b at 0.55 falls to 6.7 under protanopia,
essentially identical to clear. Red is intrinsically dark in luminance and green
intrinsically light, so the only way a red wash outweighs a green one is to be a light red.
#f38ba8 was already the state palette's red (FAULT, SENSOR_OBSERVATION.occupied), so
keeping it means no new colour enters the system and nothing needs re-validating against the
four block tints.

Also

POINT_CONFIRMATION.mismatch named diag-occupied and is nulled alongside it. Nothing ever
drew it — a confirmation is a badge, not an area, so its glyph (✗) has always been that
record's non-colour carrier — and naming a pattern id that no longer exists is worse than
naming none.

What this does not change

Occupancy remains a fill and a lock remains a line along the road (D2). The four validated
block tints are untouched (D4) — this changes the opacity of a state wash, and identity
still gives up the colour channel wherever state is drawn (D1). The sensor observation mark
keeps OCCUPANCY.occupied's hue at its own scale (D8); it is a 7px circle-and-line, never
an area, so wash opacity does not reach it.

Tested

New tests in diagram/encoding.test.ts pin the rule rather than the pixels: only the
fail-safe state carries a hatch, OCCUPANCY_WASH_OPACITY.occupied must exceed clear's,
clear/unknown stay at BLOCK_TINT_OPACITY, and every state still ships a distinct glyph
and label.

Run in this session, from the repo root:

npm run lint                                        clean
npx tsc -p packages/frontend/tsconfig.json --noEmit  exit 0

npm test
  backend    Test Files  84 passed (84)    Tests  1500 passed (1500)
  frontend   Test Files  27 passed (27)    Tests   332 passed (332)

npm run test:e2e
  71 passed (26.3s)

Docs

In the same commit: D10 added to docs/diagram-encoding.md with the numbers and the
rejected alternatives, D2's "unknown is the most distinct" paragraph amended, docs/liveness.md
M5's treatment table, docs/track-editor.md D18 (which narrated the retired hatch's
alignment bug), plus the docs/current-state.md long form and the CLAUDE.md index line.

Operator feedback: the hashing on an occupied block stands out too much.
It does, and the diagram's own reasoning is why that matters. A hatch is
the loudest non-colour mark the diagram has, and `unknown` is meant to be
the most visually assertive of the three occupancy states because it is
the one that refuses routes. Spending a hatch on `occupied` - the state a
working railway sits in most of the time - put the busiest texture on the
ordinary case and left the fail-safe state shouting over it.

The rule that replaces it (`docs/diagram-encoding.md` D10): texture
separates a fault from an operational state, never one operational state
from another. `unknown` keeps its cross-hatch and is now the only hatch on
the diagram; the `diag-occupied` pattern and its `<pattern>` def are gone.

Two flat washes cannot be told apart by hue alone, and that is not
rhetorical for this pair: composited over the `#1e1e2e` tile at the same
opacity, `#f38ba8` and `#a6e3a1` land 6.9 apart in RGB under simulated
deuteranopia - occupied and clear become the same colour for roughly 8% of
men. So the flat wash carries its own non-colour distinction:
`OCCUPANCY_WASH_OPACITY` draws occupied at 0.55 and the other two at the
ordinary `BLOCK_TINT_OPACITY` (0.26). That is a 1.57:1 luminance step,
taking deuteranope separation to 34.8 and protanope to 22.3. With colour
removed entirely, occupied is the heavier block - the right direction,
since it is also the one carrying a train - and the run label's glyph is
unchanged on top of that.

A deeper red is the instinct and is the wrong one: `#e64553` and `#ff6b6b`
both read as more emphatically red to normal vision and worse to everyone
else (`#ff6b6b` at 0.55 falls to 6.7 under protanopia, indistinguishable
from clear). Red is intrinsically dark and green intrinsically light, so
the only red wash that outweighs a green one is a light red. `#f38ba8` was
already the state palette's red, so no new colour enters the system.

`POINT_CONFIRMATION.mismatch` named `diag-occupied` and is nulled with it -
nothing ever drew it (a confirmation is a badge, not an area, so its glyph
has always been the carrier), and naming a pattern id that no longer exists
is worse than naming none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bazauto
bazauto enabled auto-merge (squash) August 27, 2026 11:15
@bazauto
bazauto merged commit c648ff4 into main Aug 27, 2026
1 check passed
@bazauto
bazauto deleted the feat/flat-occupied-wash branch August 27, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant