fix(client): PR #192 review round — per-rung legend, Region coalescing + clamp boundary tests, halving-loop honesty

All Hoshe/Araminta findings addressed (Tyre approved outright), none
retracted — plus a Dudley stop-and-flag discovery that improved on the
asked-for fix:

- Legend 100x lie (Araminta, blocking): subtitle computed via
  spacing_for_rung() and refreshed at all three _held_granularity_v2
  write sites. Region test asserts 204.800 km/cell; the District
  direction needed a stale-header-aware helper — a District-only test
  spuriously passes against the old literal by coincidence.
- Region coalescing coverage (Hoshe 1): both directions tested
  (Region-vs-District separate slots; Region-vs-Region coalesces).
- Clamp boundary tests + dangling citations (Hoshe 2): writing the
  requested halving-loop-fires test surfaced that the loop is PROVABLY
  UNREACHABLE at current constants (per-axis clamp forecloses it —
  brute-forced independently on both server and client sides). Ruling:
  the loop stays as defensive code; the test became a property sweep
  pinning both the wire-cap invariant and the loop's no-op status (a
  future constant change breaks it loudly); doc comments on both sides
  drop the load-bearing framing and state the truth; the old client
  mirror test that claimed the loop fires (passing on the per-axis
  clamp alone) is replaced the same way. Client citations now name the
  real server tests verbatim.
- Governance (Tyre): D-226 amendment note — progressive cross-rung
  refinement EXTENDS T-1124 §4 (not supersedes); legacy u32 field
  scheduled for retirement (T-1159).

Server: 1818 lib tests green, clippy/fmt clean. Client: zoom_ladder
48/48, window_request 26/26, viewer 74/74; gdlint clean. Every fix
revert-verified.
This commit is contained in:
2026-07-22 17:24:07 +02:00
parent 8517cae66f
commit da15261276
7 changed files with 345 additions and 63 deletions
@@ -16,6 +16,7 @@ const PANEL_MARGIN: float = 16.0
const LEGEND_PANEL_WIDTH: float = 260.0
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## The morphology base layer folds its 17 zones into ~5 family rows (§5:
## "mirroring T-1112's 'not everything earns permanent screen space'
@@ -55,13 +56,28 @@ func reposition() -> void:
## Always shows the base-layer key (morphology + elevation reading, always
## on) plus glaciation (always-on modifier), then whichever toggle overlay is
## currently active, if any.
##
## The subtitle's km/cell reading is NOT a fixed "district window" label —
## it was until this fix hardcoded District's own 2.048 km/cell, which was a
## 100x lie whenever the viewer actually holds Region (204.8 km/cell) or
## Quarter (0.512 km/cell). Read through AtlasWindowGeometry.
## spacing_for_rung() — the SAME pure lookup _refresh_screen_header() uses
## for the main screen header's subtitle (screen_header_content()) — keyed
## off the viewer's current get_held_granularity_v2(), so the legend and
## header can never disagree. AtlasWindowViewer calls refresh() at every
## point _held_granularity_v2 changes (_enter_tile_mode(), _enter_at_rung(),
## _on_window_ready()'s rung-swap adoption) — see those call sites.
func refresh() -> void:
if _viewer == null:
return
clear()
visible = true
add_component(ImplantHeader.new("REGIONAL LEGEND", "district window · 2.048 km/cell"))
var spacing_km: float = AtlasWindowGeometry.spacing_for_rung(_viewer.get_held_granularity_v2()) / 1000.0
var subtitle: String = "%s window · %.3f km/cell" % [
_viewer.get_held_granularity_v2().to_lower(), spacing_km
]
add_component(ImplantHeader.new("REGIONAL LEGEND", subtitle))
add_component(ImplantSeparator.new())
_add_morphology_section()
@@ -25,8 +25,8 @@ extends Control
## keeps drawing — progressive refinement, no blank frame (§6). A pan
## past the held window's edge re-requests the SAME rung at a new center.
## - Zooming fully out snaps to the CANONICAL planetary frame (Jeroen's HARD
## condition) — see _maybe_reset_to_canonical_frame() — which, on a body
## needing tiling, re-enters `_tile_mode` (live round 3, design doc §4).
## condition, see _maybe_reset_to_canonical_frame()) — on a body needing
## tiling, re-enters `_tile_mode` (live round 3, design doc §4).
## - _window_request (atlas_window_request.gd) owns the single-window
## cache/debounce/retry; _tile_set (atlas_window_tile_set.gd) owns N of
## those for the tiled rest state — this Control decides WHICH is active.
@@ -34,8 +34,7 @@ extends Control
## Navigation (Jeroen's input-model ruling: LMB-drag panning BREAKS click
## semantics with future map objects, so it's removed entirely):
## WASD / arrow keys continuous pan, held (frame-rate independent, _process)
## Edge scrolling cursor within EDGE_SCROLL_MARGIN_PX of a viewport
## edge pans toward it (suppressed over UI / unfocused)
## Edge scrolling cursor within EDGE_SCROLL_MARGIN_PX pans toward it (suppressed over UI / unfocused)
## Mouse wheel cursor-anchored zoom; crosses rungs continuously (T-1153)
## Esc back (nav.pop())
@@ -47,14 +46,13 @@ const OVERLAY_BAR_HEADER_RESERVE: float = 360.0
## T-1153: MIN_ZOOM/MAX_ZOOM are a wide safety clamp on the raw display
## multiplier, NOT a rung boundary — wheel zoom is CONTINUOUS and UNCLAMPED
## ACROSS RUNGS (D-013): crossing a rung's coverage ceiling
## (AtlasWindowGeometry.select_rung()) re-requests a DIFFERENT granularity
## at the SAME apparent screen extent, never clamping _view_zoom itself.
## (AtlasWindowGeometry.select_rung()) re-requests a DIFFERENT granularity at
## the SAME apparent screen extent, never clamping _view_zoom itself.
## set_view() (T-1120 capture API) clamps to this same range independently.
##
## MIN_ZOOM must stay low enough that fit_window_view()'s COVER fit for
## enter_orbital()'s largest legal `n` is never itself clamped — that would
## silently show LESS than the whole body, breaking Jeroen's HARD condition.
## 0.0005 covers a ~120,000 km-radius body at a 3840px 4K viewport.
## enter_orbital()'s largest legal `n` is never itself clamped (would
## silently show LESS than the whole body). 0.0005 covers a ~120,000 km-
## radius body at a 3840px 4K viewport.
const MIN_ZOOM: float = 0.0005
const MAX_ZOOM: float = 64.0
const ZOOM_STEP: float = 1.15
@@ -66,10 +64,9 @@ const ZOOM_STEP: float = 1.15
const PAN_SPEED_CANVAS_PX_S: float = 96.0
## T-1145 item 2: cursor-to-edge distance (px) that triggers edge-scroll —
## Jeroen's own number ("~24px").
## Jeroen's own number ("~24px"). Uses the SAME speed as WASD (one pan feel,
## two triggers) — no separate constant, _process() reads PAN_SPEED_CANVAS_PX_S for both.
const EDGE_SCROLL_MARGIN_PX: float = 24.0
## Edge-scroll uses the SAME speed as WASD (one pan feel, two triggers) —
## no separate constant, _process() reads PAN_SPEED_CANVAS_PX_S for both.
## Pixel size of one district cell at zoom=1.0 — a fixed on-screen scale
## (unlike AtlasViewer's heightmap, there is no source texture dictating a
@@ -82,17 +79,14 @@ const COLOR_BG: Color = Color("#0d1117")
## Border-fade target (§5 "what renders during the wait"): the underlying
## whole-body heightmap's own background tint, so the newly-exposed edge
## reads as "real data seen through", not a placeholder block. Reuses
## AtlasViewer's own COLOR_HEIGHTMAP_TINT-adjacent dim value rather than
## inventing a new one — this IS a dimmer/less-certain read of the same
## planetary data, not a different visual language.
## AtlasViewer's own COLOR_HEIGHTMAP_TINT-adjacent dim value — a dimmer/
## less-certain read of the same planetary data, not a different visual language.
const COLOR_BORDER_FADE: Color = Color(0.20, 0.24, 0.30, 0.55)
## T-1153/R6: pending-refinement wash — the border-fade's referent repointed
## to "the previous derived composite at this position" for a rung-crossing
## zoom (progressive refinement leaves real data on screen, unlike the
## no-composite-yet case COLOR_BORDER_FADE covers). Same hue family, much
## lighter alpha — a hint that something sharper is arriving, not a claim
## that the current view is empty or wrong.
## T-1153/R6: pending-refinement wash — border-fade's referent repointed to
## "the previous derived composite at this position" for a rung-crossing zoom
## (real data stays on screen, unlike the no-composite-yet case above). Same
## hue, lighter alpha — hints something sharper is arriving, not that the view is wrong.
const COLOR_PENDING_REFINEMENT_WASH: Color = Color(0.20, 0.24, 0.30, 0.12)
## D-243: 2,048 m per district side.
@@ -311,6 +305,7 @@ func _enter_tile_mode(body: Dictionary, system: Dictionary, radius_km: float) ->
_window_request.reset()
_tile_set.enter(_dict_str(_body, "body_id", ""), radius_km)
_refresh_screen_header()
_legend_panel.refresh()
grab_focus()
queue_redraw()
_overlay_node.queue_redraw()
@@ -346,6 +341,7 @@ func _enter_at_rung(
_dict_str(_body, "body_id", ""), _held_center, clamped_n, granularity_v2
)
_refresh_screen_header()
_legend_panel.refresh()
grab_focus()
queue_redraw()
_overlay_node.queue_redraw()
@@ -413,6 +409,12 @@ func get_held_n() -> int:
return _held_n
## Currently-HELD rung tag — legend reads this via spacing_for_rung(),
## mirroring _refresh_screen_header()'s own use of the field.
func get_held_granularity_v2() -> String:
return _held_granularity_v2
## Live round 5: current body's radius — mosaic draw needs `cols` for
## nearest_wrap_image()'s wrap resolution. Mirrors the
## `_body.get("body_radius_km", 0.0)` pattern used throughout this file.
@@ -495,6 +497,7 @@ func _on_window_ready(window: Dictionary) -> void:
_fit_and_center()
_awaiting_first_window = false
_refresh_screen_header()
_legend_panel.refresh()
queue_redraw()
_overlay_node.queue_redraw()
@@ -968,12 +971,9 @@ func _notification(what: int) -> void:
_position_overlay_bar()
if _legend_panel:
_legend_panel.reposition()
# T-1142: re-fit on resize too, same _user_adjusted guard as the other
# two auto-fit events (enter, first window arrival) — never fights a
# manually-adjusted view. _canvas guard matches _overlay_bar/
# _legend_panel above: NOTIFICATION_RESIZED can fire mid-_ready()
# (anchor_right/anchor_bottom assignment triggers it) BEFORE _canvas
# is constructed — confirmed the hard way (gdUnit add_child() crash).
# T-1142: re-fit on resize (_user_adjusted guard, as other auto-fit
# events). _canvas guard: NOTIFICATION_RESIZED can fire mid-_ready()
# before _canvas exists (gdUnit add_child() crash, confirmed).
if _canvas and not _user_adjusted:
_fit_and_center()
elif what == NOTIFICATION_APPLICATION_FOCUS_OUT: