docs(meta): D-261 — rivers are a stroke, not a scaled feature
A river is sub-pixel at almost every Atlas scale (a 100 m channel against Global's ~17.6 km per screen pixel), so drawing it to scale draws nothing. The line becomes a symbol whose job is legibility: a fixed 5 px screen-space stroke, contiguous through the river's own cell centres, never drawn over water, and culled when too small to read. The cull threshold derives from the stroke rather than being stipulated. A mark reads as a line at roughly 3x its own width, so the minimum is 15 px of on-screen length — and the kilometre thresholds then fall out of each rung's scale: 264 km at Global, 2.8 km at Region, 28 m at District. A level-of-detail ladder with no hand-tuned constants, which self-corrects if the width changes. Measured on the visible extent, not total river length: a course crossing the window always spans it and passes, so only a course wholly inside the view and small is culled. Correct at both ends of the ladder, and no new wire field. Records a tension that had to be resolved rather than split. Jeroen proposed a flat 100 km cutoff, then noted that 5x5 px still reads as a dot and asked for 15-20. Those are incompatible: the km threshold depends only on the pixel length, so 100 km implies ~6 px, which is dot-shaped at a 5 px stroke. Admitting 100 km rivers AND keeping them line-shaped needs a ~2 px stroke. Thick lines mean fewer rivers; thin lines mean more. Ruled for the bold stroke. Client-side, and provably so: the cull is measured in screen pixels, and the display ratio is a client-side viewport-dependent parameter that never reaches a wire request or cache key, so the server cannot know how many pixels a course occupies. Water-clipping needs nothing new either — the per-cell classification is already in the adopted canvas. Built on the 2026-07-27 measurement in D-258's amendment: 375 courses present, 458 of 518,400 pixels different. The rivers were not failing to render, they were correctly beneath notice; the defect was drawing specks instead of drawing properly or not at all. T-1237 implements. T-1238 restores size-varying width as polish, deliberately deferred — noting width and cull are coupled, since a thinner stroke lowers its own visibility threshold. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -326,6 +326,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it.
|
||||
- [D-258: Rung-0.5 expanded layer — one derived base for the whole ladder](decisions/architecture.md#d-258-rung-05-expanded-layer--one-derived-base-for-the-whole-ladder) — _architecture_
|
||||
- [D-259: Enclosed settlements — open-air requires a breathable atmosphere](decisions/architecture.md#d-259-enclosed-settlements--open-air-requires-a-breathable-atmosphere) — _architecture_
|
||||
- [D-260: Generator scope — Sol is authored, not generated](decisions/architecture.md#d-260-generator-scope--sol-is-authored-not-generated) — _architecture_
|
||||
- [D-261: River rendering — a stroke, not a scaled feature](decisions/architecture.md#d-261-river-rendering--a-stroke-not-a-scaled-feature) — _architecture_
|
||||
|
||||
## Open questions
|
||||
|
||||
|
||||
@@ -2448,4 +2448,37 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
|
||||
---
|
||||
|
||||
*114 decisions (D-001 through D-260, excluding gaps). Last updated: 2026-07-27 (D-259 — enclosed settlements: open-air requires a breathable atmosphere, branch on an explicit property, D-220's density model declared inapplicable to pressure vessels; D-260 — generator scope: Sol encoded as `Authored { deepest_rung: Global }`, the DLC gate a depth not a boolean, Sol art an authored data canvas).*
|
||||
### D-261: River rendering — a stroke, not a scaled feature
|
||||
|
||||
- **Date:** 2026-07-28
|
||||
- **Decision:** A river on an Atlas canvas is drawn as a **cartographic stroke**, never as terrain scaled to its true width. Four rules, all client-side:
|
||||
- **Fixed 5 px width, screen-space, identical at every rung.** A real river is sub-pixel at almost every Atlas scale (a 100 m channel against Global's ~17.6 km per screen pixel), so drawing it to scale draws nothing. The line is a *symbol* whose job is legibility, not a measurement.
|
||||
- **Contiguous, through the river's own cells.** The polyline passes through each river cell's centre, so its geometry is a function of the river, not of how far out the player is zoomed.
|
||||
- **Never drawn over water.** A course is truncated where it meets ocean or lake. A blue line across open sea asserts something false.
|
||||
- **Culled below 15 px of on-screen length** — see the derivation below.
|
||||
- **The cull threshold is derived from the stroke, not stipulated.** A mark only reads as a *line* at roughly 3–4× its own width; at 5 px wide and 5 px long it is a square. The rule is therefore **minimum on-screen length = 3 × stroke width = 15 px**, and the kilometre thresholds fall out of each rung's own scale rather than being written down:
|
||||
|
||||
| Rung | ground per screen px | river must exceed |
|
||||
|---|---|---|
|
||||
| Global | 17.6 km | **264 km** |
|
||||
| Region | 190 m | 2.8 km |
|
||||
| District | 1.9 m | 28 m |
|
||||
| Chunk | 0.06 m | 0.9 m |
|
||||
|
||||
This is a genuine level-of-detail ladder with no hand-tuned constants: from orbit only major systems are visible, and tributaries emerge on the way down, which is how an atlas behaves. It self-corrects — change the stroke width and every threshold moves with it.
|
||||
|
||||
**Measure the VISIBLE extent, not total river length.** A course crossing the window always spans it and so always passes; only a course lying wholly inside the view *and* small is culled. That is exactly "too small to see", it behaves correctly at both ends of the ladder, and it requires no new wire field.
|
||||
|
||||
**Note the tension resolved here.** Jeroen first proposed a flat 100 km cutoff, then observed that 5 px × 5 px still reads as a dot and asked for 15–20 px. Those are incompatible: the kilometre threshold depends **only** on the pixel length (stroke width does not enter it), so 100 km at Global implies ~6 px, which is dot-shaped at a 5 px stroke. Admitting 100 km rivers *and* keeping them line-shaped would require dropping the stroke to ~2 px. **Thick lines mean fewer rivers; thin lines mean more.** Ruled in favour of the bold stroke and the 264 km cutoff: at whole-planet scale an atlas shows major systems only, and weakening the rivers that matter to admit ones you can barely follow is a poor trade.
|
||||
- **This is a CLIENT function over server data, and cannot be otherwise.** The split follows [D-255](#d-255)'s own premises: the **server** owns which courses exist, where their cells run, and their class (content determination); the **client** is the map-art function, so width, culling and water-clipping are all presentation. The cull in particular *cannot* move server-side — it is measured in screen pixels, and the display ratio is explicitly a client-side, viewport-dependent parameter that never touches a wire request or a cache key, so the server cannot know how many pixels a course occupies. Water-clipping likewise needs nothing new over the wire: the per-cell classification is already in the canvas the client was sent.
|
||||
- **Compute once per canvas adoption (`set_frame`), never per draw.** The annotation layer redraws every frame; a per-point water lookup across several hundred courses every frame would be waste for geometry that only changes when a new canvas arrives.
|
||||
- **Rationale:** Measured, not assumed. With course suppression disabled experimentally on 2026-07-27, Ferrath's Global canvas carried **375 courses** and differed from courses-off by **458 of 518,400 pixels**. The rivers were not failing to render — they were correctly beneath notice, because station spacing was tied to the *canvas* pitch, so a course shorter than one gridunit collapsed to a fragment or a dot. The defect was drawing them as specks rather than either drawing them properly or not at all. Both halves of this record address that: contiguity fixes the geometry, the cull fixes the specks.
|
||||
- **Raised by:** Jeroen, pair session 2026-07-27/28: *"the rivers should probably not be rendered at scale, but as a 5 px contiguous blue line curving to follow the river pixels as their center point. These lines should never be drawn in a pixel that is water blue, since that makes no sense"*, then *"the ones that are sub pixel are logically invisible from orbit"*, then *"if the width is five and the length is five it will still read as a dot"*.
|
||||
- **Deferred — width varying with river size.** The existing ratified stream/tributary/trunk width grammar (0.6 / 1.2 / 2.4 px, tuned so a tributary joining a trunk reads as a join) **collapses to a uniform 5 px under this record**. Jeroen: *"I do like your suggestion of thinning shorter rivers (especially if that shows tributaries following those rules) but maybe that is for a future polish iteration."* Recorded so the flattening is understood as a deliberate deferral rather than a lost feature — and note the thinning idea composes naturally with the cull, since a thinner stroke lowers its own visibility threshold and would let smaller tributaries appear at the same rung.
|
||||
- **Implementation:** Not yet built. Consumers: `step_canvas_annotation_layer.gd` (stroke width, contiguity, water-clip, cull — all four live here), which already holds the per-cell classification and the world→screen projection it needs.
|
||||
- **Cross-reference:** [D-255](#d-255) (stepped ladder — premise (1) server-determines-content / premise (2) client-is-a-map-art-function, the split this record applies; and the 2026-07-26 extent inversion that made screen-pixel reasoning coherent per rung), [D-258](#d-258) (rung 0.5 — its amendment records the 375-course / 458-pixel measurement this record is built on), [D-227](#d-227) (derive-don't-store — culling is presentation, not derivation, so it is outside that rule).
|
||||
- **Dissent:** None.
|
||||
|
||||
---
|
||||
|
||||
*115 decisions (D-001 through D-261, excluding gaps). Last updated: 2026-07-28 (D-261 — river rendering: a fixed 5 px screen-space stroke through cell centres, never over water, culled below 15 px of on-screen length so the km threshold derives per rung from the stroke itself; client-side because the cull is measured in pixels the server cannot know).*
|
||||
|
||||
Reference in New Issue
Block a user