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:
@@ -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
|
||||
Reference in New Issue
Block a user