fix(ui): T-1172 — clip river dots/confluences/mouths against the drawn waterline

Jeroen's hands-on report: rivers continuing under the ocean. Tyre's
ruling implemented: the skeleton is rung-independent, the drawn coast
is rung-indexed (warp cutoff admits more octaves per rung), so
reconciliation is a presentation-frame operation — a draw-time clip
against the SAME per-cell morphology verdict the terrain painter used,
at the rung on screen. New pure module atlas_window_water_clip.gd:
cell resolution across both paths (single-window direct; tile mode
selects the containing tile by each tile's OWN echoed n with nearest-
wrap re-expression against that tile's canonical center — response-is-
source-of-truth + wrap discipline reused, not reinvented). Strict drop
(no snap); offshore mouths suppressed (return with real termini in
T-1170 — retirement markers at the clip sites); basins untouched;
fail-open wherever no composite data has arrived (the clip refines
presentation, never gates data). The _pos split feeds the SAME wrap-
resolved district to both draw position and clip test so they can
never disagree about the wrap image. 49 new tests incl. antimeridian
and mid-progressive-arrival fail-open; revert-verified with precise
attribution (breaking water detection fails exactly the 3 water tests,
fail-open/land tests stay green); smoke-suite stub crash under a real
driver caught and fixed (headless skip-gating masked it). 7 suites
regression-free; gdlint clean.

Tickets: T-1172

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 10:37:56 +02:00
co-authored by Claude Fable 5
parent 7c5dc35b38
commit 9914dc0d91
6 changed files with 691 additions and 11 deletions
@@ -39,6 +39,21 @@ const MORPHOLOGY_COLORS: Array = [
Color(0.25, 0.45, 0.40, 0.55), # 16 Wetland
]
## MorphologyZone discriminants that read as DRAWN WATER — T-1172 (the
## river-skeleton waterline-clip fix): the raw heightmap sea level the
## server's drainage extraction filters river cells against (drainage.rs)
## and the DERIVED morphology verdict this client actually paints (which
## includes the T-1162 coast-warp invention, and at Region rung aggregates
## to 204.8 km cells) are two independently-computed waterlines that can
## legitimately disagree — Tyre's ruling (client draw-time clip, no single
## server waterline is well-defined) reads THESE two discriminants as "drawn
## water" to suppress a river dot/mouth against. Matches D-239 SS6's own
## ordering exactly (0=OpenOcean, 1=Lake) — the only two MorphologyZone
## values that are actually open water; every other zone (TidalFlat onward)
## is drawn land or a land/water transition zone, not water itself.
const MORPHOLOGY_OPEN_OCEAN: int = 0
const MORPHOLOGY_LAKE: int = 1
## Sub-biome -> marker color (Araminta's palette, D-226). Grouped pairs share
## a color since they read the same on the map.
const SUB_BIOME_COLORS: Dictionary = {
@@ -169,6 +184,16 @@ static func morphology_color(zone: int) -> Color:
return Color(0.5, 0.5, 0.5, 0.4)
## T-1172: whether a MorphologyZone discriminant reads as drawn water —
## see MORPHOLOGY_OPEN_OCEAN/MORPHOLOGY_LAKE's own doc for the two-waterline
## rationale. An out-of-range zone (a palette/enum drift bug) is NOT water —
## matches district_window_morphology_color()'s own "unrecognized -> loud
## magenta, not silently treated as any known category" posture; an
## unrecognized zone must never silently suppress a river dot.
static func is_morphology_water(zone: int) -> bool:
return zone == MORPHOLOGY_OPEN_OCEAN or zone == MORPHOLOGY_LAKE
static func sub_biome_color(sub_biome: String) -> Color:
return SUB_BIOME_COLORS.get(sub_biome, COLOR_SUB_BIOME_DEFAULT)
@@ -61,6 +61,8 @@ extends Node2D
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
# T-1172: two-waterline clip — see that file's own header doc.
const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd")
## Reused verbatim from the retired atlas_marker_overlay.gd (Araminta's
## ruling: "reuse the retired palette exactly") — same values, same source of
@@ -210,11 +212,19 @@ func _zs(screen_space_size: float, ctx: Dictionary) -> float:
return AtlasWindowGeometry.zoom_compensated_size(screen_space_size, ctx["view_zoom"])
## Pixel (row, col) -> canvas-local, wrap-resolved to whichever longitude
## image is nearest the currently-held view — the SAME two-step
## (map-then-nearest-wrap) the tile mosaic draw path uses, just for a single
## point instead of a tile's four corners.
func _pos(row: float, col: float, ctx: Dictionary) -> Vector2:
## Pixel (row, col) -> fractional district position, wrap-resolved against
## the currently-HELD view's own center (ctx["held_center"]) — the
## representative wrap-image _pos()'s canvas conversion needs. T-1172: this
## is also the value fed to the water-clip lookup (_is_drawn_water()) — NOT
## a separately-computed position — so the clip test and the actual drawn
## position can never disagree about which longitude wrap-image is meant.
## Tile-mode's own per-tile wrap re-resolution (AtlasWindowWaterClip.
## resolve_morphology_zone()) re-derives whichever wrap-image a SPECIFIC
## tile needs internally; feeding it this held-center-wrapped value is a
## safe, consistent starting representative either way (longitude is
## periodic — any wrap-image of the same district resolves to the same
## real-world position).
func _district(row: float, col: float, ctx: Dictionary) -> Vector2:
var world_m: Vector2 = AtlasWindowGeometry.layer1_pixel_to_world_m(
row, col, ctx["grid_w"], ctx["grid_h"], ctx["radius_km"]
)
@@ -229,11 +239,51 @@ func _pos(row: float, col: float, ctx: Dictionary) -> Vector2:
# integer rounding would otherwise discard — river dots are not
# district-lattice-snapped (see world_m_to_district()'s own doc).
district.x = wrapped_col + (district.x - roundi(district.x))
return district
## Pixel (row, col) -> canvas-local, wrap-resolved to whichever longitude
## image is nearest the currently-held view — the SAME two-step
## (map-then-nearest-wrap) the tile mosaic draw path uses, just for a single
## point instead of a tile's four corners. Thin wrapper over _district() +
## AtlasWindowGeometry.district_to_canvas_local() (T-1172 split: callers that
## also need the water-clip test call _district() directly instead, so the
## SAME resolved district feeds both the draw position and the clip check).
func _pos(row: float, col: float, ctx: Dictionary) -> Vector2:
return AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
_district(row, col, ctx), ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
## T-1172: whether the composite cell covering `district` is drawn as water
## (OpenOcean/Lake) — FAILS OPEN (returns false, "not water", i.e. draw the
## dot) when no arrived composite data covers the position, per Tyre's rule
## 5 ("the clip is a presentation refinement, never a data gate"). Resolves
## through AtlasWindowWaterClip.resolve_morphology_zone(), which handles
## BOTH the single-window rung path and Region tile mode internally — this
## function never branches on viewer.is_tile_mode() itself, matching that
## function's own "single dispatch point" doc.
func _is_drawn_water(district: Vector2, ctx: Dictionary) -> bool:
var is_tile_mode: bool = viewer.is_tile_mode()
var single_window: Variant = null if is_tile_mode else viewer.get_district_window()
var tiles: Array = []
if is_tile_mode:
var tile_set = viewer.get_tile_set()
if tile_set != null:
tiles = tile_set.get_tiles()
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
district, is_tile_mode, single_window, tiles, ctx["cols"]
)
if zone == AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA:
return false
return AtlasOverlayColors.is_morphology_water(zone)
## T-1172 clip — retire when T-1170 course invention terminates courses at
## the invented coast. River cells, confluences, and mouths are each dropped
## (strict, no snap) when their resolved composite cell reads as drawn water
## — see AtlasWindowWaterClip's own header doc for the two-waterline
## rationale. Basins are explicitly OUT OF SCOPE (Tyre's rule 4) — untouched.
func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
var granularity_v2: String = ctx["granularity_v2"]
var river_cells: Array = rn.get("river_cells", [])
@@ -251,7 +301,13 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
)
if not AtlasWindowGeometry.river_class_visible_at_rung(cls, granularity_v2):
continue
var p: Vector2 = _pos(float(c[0]), float(c[1]), ctx)
var district: Vector2 = _district(float(c[0]), float(c[1]), ctx)
# T-1172 clip — retire when T-1170 course invention terminates at the invented coast.
if _is_drawn_water(district, ctx):
continue
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
if is_region:
var radius: float = AtlasWindowGeometry.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
draw_circle(p, _zs(radius, ctx), COLOR_GEN_RIVER)
@@ -269,14 +325,30 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
if AtlasWindowGeometry.confluences_visible_at_rung(granularity_v2):
for cf: Variant in rn.get("confluences", []):
if cf is Array and cf.size() >= 2:
var p: Vector2 = _pos(float(cf[0]), float(cf[1]), ctx)
var district: Vector2 = _district(float(cf[0]), float(cf[1]), ctx)
if _is_drawn_water(district, ctx):
continue
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
var radius: float = _zs(AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, ctx)
draw_circle(p, radius, COLOR_GEN_RIVER)
if AtlasWindowGeometry.mouths_visible_at_rung(granularity_v2):
for m: Variant in rn.get("mouths", []):
if m is Array and m.size() >= 2:
_draw_mouth(_pos(float(m[0]), float(m[1]), ctx), ctx)
var district: Vector2 = _district(float(m[0]), float(m[1]), ctx)
# T-1172 rule 3: mouths are SUPPRESSED (not snapped, not
# dimmed) when their cell reads as drawn water — a mouth is
# the worst-case disagreement by construction (the last LAND
# cell on the RAW coast; wherever the drawn coast is
# displaced inland, the mouth renders offshore).
if _is_drawn_water(district, ctx):
continue
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
_draw_mouth(p, ctx)
## Double-ring sea-terminus marker — verbatim geometry from the retired
@@ -0,0 +1,140 @@
extends RefCounted
## T-1172 — the river-skeleton waterline-clip fix. Pure geometry, split into
## its own file (not folded into atlas_window_geometry.gd, which is already
## close to the gdlint max-file-lines cap): the river skeleton's own SOURCE
## (server/src/atlas/drainage.rs) is filtered against the RAW heightmap sea
## level, but the DRAWN ocean this client actually paints is the derived
## MorphologyZone verdict (server/src/atlas/district_profile.rs) — which
## post-T-1162 includes the coast-warp invention (the drawn coastline is
## deterministically displaced from the heightmap coast) and, at Region
## rung, aggregates to 204.8 km cells. These are two independently-computed
## waterlines that can legitimately disagree; Tyre's ruling (T-1172): no
## single server waterline is well-defined, so the fix is a CLIENT draw-time
## clip against whichever composite cell is currently ON SCREEN at a given
## river dot's position — strict drop, no snap (a dot that lands on drawn
## water is simply not drawn; Region's 205 km cells may amputate a river's
## final coastal dots, an accepted cost per the ruling). T-1172 clip —
## retire when T-1170 course invention terminates courses at the invented
## coast.
##
## const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd")
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## Sentinel returned by the lookups below when no arrived composite data
## covers the queried position — either the position is outside every
## held/tiled window's own extent, or the window/tile at that position
## hasn't arrived yet. The caller (AtlasWindowNatureOverlay) must FAIL OPEN
## on this sentinel (draw the dot) — Tyre's rule 5: the clip is a
## presentation refinement, never a data gate. Chosen as -1 (not a legal
## MorphologyZone discriminant, which is always >= 0) so it can never be
## mistaken for a real "not water" zone.
const MORPHOLOGY_ZONE_NO_DATA: int = -1
## The derived per-cell grid side length (CELLS) for a window dict `w` — a
## DELIBERATE duplicate of AtlasWindowOverlay.cell_grid_side_for_window(),
## not a shared call, matching this codebase's own "each file owns its own
## reading of a small pure lookup rather than force a dependency" precedent
## (atlas_overlay_colors.gd's header doc states this explicitly for the
## color-palette case; the SAME rationale applies here: atlas_window_overlay.gd
## already depends on atlas_window_geometry.gd, so a dependency back from
## there — or from this file, if it lived there — would risk a circular or
## at least confusing import graph). Mirrors
## server/src/atlas/layer_proxy.rs's `WindowGranularity::cell_grid_side`
## exactly, matching the canonical function's own doc byte-for-byte in intent.
static func cell_grid_side_for_window(w: Dictionary) -> int:
var n: int = int(w.get("n", 0))
var granularity_v2 := str(w.get("granularity_v2", "District"))
match granularity_v2:
"Quarter":
return n * 4
"Region":
return maxi(roundi(float(n) / 100.0), 1)
_:
return n
## Resolve a FRACTIONAL district position to the MorphologyZone discriminant
## of the composite cell covering it, for a SINGLE window dict `w` (the
## single-window rung path: District/Quarter, and each individual Region
## tile in tile mode share this same per-window shape). Returns
## MORPHOLOGY_ZONE_NO_DATA if `w` is null/malformed, has no morphology array,
## or `district` falls outside `w`'s own `[center - n/2, center + n/2)`
## extent (the SAME containment convention
## AtlasWindowGeometry.district_to_canvas_local() uses, so a position judged
## "inside" here is exactly the position that would draw as part of THIS
## window's composite on screen — no separate containment rule to drift out
## of sync with the actual paint).
static func morphology_zone_in_window(district: Vector2, w: Variant) -> int:
if not w is Dictionary:
return MORPHOLOGY_ZONE_NO_DATA
var window: Dictionary = w
var center_raw: Variant = window.get("center", [0, 0])
var center: Vector2i = (
Vector2i(int(center_raw[0]), int(center_raw[1])) if center_raw is Array else Vector2i.ZERO
)
var n: int = int(window.get("n", 0))
if n <= 0:
return MORPHOLOGY_ZONE_NO_DATA
var half: float = float(n) * 0.5
var local_x: float = district.x - (float(center.x) - half)
var local_y: float = district.y - (float(center.y) - half)
if local_x < 0.0 or local_x >= float(n) or local_y < 0.0 or local_y >= float(n):
return MORPHOLOGY_ZONE_NO_DATA
var morphology: Variant = window.get("morphology")
if not (morphology is PackedByteArray or morphology is Array):
return MORPHOLOGY_ZONE_NO_DATA
var grid_side: int = cell_grid_side_for_window(window)
if grid_side <= 0:
return MORPHOLOGY_ZONE_NO_DATA
var col: int = clampi(int(floor(local_x / float(n) * float(grid_side))), 0, grid_side - 1)
var row: int = clampi(int(floor(local_y / float(n) * float(grid_side))), 0, grid_side - 1)
var idx: int = row * grid_side + col
if idx < 0 or idx >= morphology.size():
return MORPHOLOGY_ZONE_NO_DATA
return int(morphology[idx])
## Resolve a fractional district position to a MorphologyZone discriminant
## across BOTH viewer modes — the single dispatch point
## AtlasWindowNatureOverlay's clip predicate calls, so it never needs its own
## is_tile_mode() branch. Single-window mode: one direct
## morphology_zone_in_window() call against `single_window`. Tile mode:
## linear scan of `tiles` (Array of {"center": Vector2i, "window": Variant},
## AtlasWindowTileSet.get_tiles()'s own shape) for whichever tile's
## `[center - TILE_N/2, center + TILE_N/2)` extent contains the position —
## each tile's OWN echoed `window["n"]` is used for the actual containment
## test (not TILE_N assumed), matching this cluster's "the response is the
## source of truth for what it actually contains" precedent, since a
## clamped/still-arriving tile's real extent can differ from the nominal
## per-tile request size. `district.x` is WRAP-RESOLVED against each tile's
## own canonical center via AtlasWindowGeometry.nearest_wrap_image() before
## the containment test — a tile's `center` is always canonical (wrapped
## into `[0, cols)`), but the queried district position may be expressed in
## a DIFFERENT wrap-image (e.g. a river dot near the antimeridian) — the
## same "re-express before comparing" discipline
## AtlasWindowOverlay._draw_tile_mosaic() and AtlasWindowNatureOverlay's own
## _pos() already use. `cols <= 0` (no-radius body) is a safe no-op
## passthrough (nearest_wrap_image()'s own contract).
static func resolve_morphology_zone(
district: Vector2, is_tile_mode: bool, single_window: Variant, tiles: Array, cols: int
) -> int:
if not is_tile_mode:
return morphology_zone_in_window(district, single_window)
for tile: Dictionary in tiles:
var window: Variant = tile.get("window")
if not window is Dictionary:
continue
var tile_center: Vector2i = tile.get("center", Vector2i.ZERO)
var wrapped_x: float = district.x
if cols > 0:
var wrapped_col: int = AtlasWindowGeometryRef.nearest_wrap_image(
roundi(district.x), tile_center.x, cols
)
wrapped_x = float(wrapped_col) + (district.x - roundi(district.x))
var zone: int = morphology_zone_in_window(Vector2(wrapped_x, district.y), window)
if zone != MORPHOLOGY_ZONE_NO_DATA:
return zone
return MORPHOLOGY_ZONE_NO_DATA