Files
settled-reach/client/ui/implant/apps/atlas/atlas_app.gd
T
jpmschweitzerandClaude Fable 5 d28d24fd26 feat(ui): step-canvas map component — RTT terrain + stepped zoom (D-255, T-1182)
The two-layer client rebuild per D-255(a)(b)(e), replacing the
_canvas.scale continuous-zoom model with one viewer, one path, all six
rungs:

- step_canvas_protocol.gd: StepCanvasRequest/Response codec against
  the T-1181 wire contract — incl. the discovered png_bytes subtlety
  (rmp_serde without serde_bytes emits a msgpack int-array, not bin;
  decode repacks via PackedByteArray before load_png_from_buffer) and
  the extent-echo rule (read the server-clamped extent, never assume
  the requested one).
- step_canvas/ component: transport (six-rung ladder, cursor-anchored
  scroll steps, edge-scroll/WASD pan with re-request on edge crossing,
  hard reset-to-Global), RTT terrain layer (Image.set_pixel colorize
  per the c1 measured ruling, texture.update reuse on step-cross,
  NEAREST coarse / LINEAR fine per rung), unscaled screen-space
  annotation sibling (courses + settlement markers at literal px),
  in-memory LRU cache (Tier 1; T-1183 layers the disk tiers beneath),
  request lifecycle (pending retry, staleness gate, extent echo).
- Full _canvas.scale retirement in the same change: the zoom-scaled
  canvas model, the _zs compensation family, select_rung /
  MAX_COVERAGE_M / compute_tile_grid, the orbital-mosaic-vs-window
  two-path split, _view_zoom/_canonical_fit_zoom — 10 source files
  deleted; their 14 test suites deleted with them (T-1157 dead-goldens
  rule; replacement visual-capture coverage is re-scoped T-1157).
- Surviving surfaces kept per the ticket: atlas_window_cache.gd's LRU
  shape (the ticket's named file atlas_window_tile_set.gd was the
  retiring orchestrator; the real LRU shape lives in
  atlas_window_cache.gd — cited in step_canvas_cache.gd), overlay
  colors, legend/overlay-bar chrome, AtlasViewer descend geometry.

Determinism boundary per D-255(e): the client interpolates only within
the closed server-supplied input set. 7 new gdUnit suites (164 cases)
incl. a real extent-echo bug caught by its own test during
implementation. Full client suite green (exit 0) with the live-gated
suites running against a worktree server build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 08:53:48 +02:00

186 lines
6.4 KiB
GDScript

class_name AtlasApp
extends ImplantApp
## Atlas implant app (#844, #836, D-191).
## Reach map → system orbital → planet entry → regional zoom ladder.
## Registered as "implant/map" in FULLSCREEN mode.
##
## T-1153 (D-226 T-1143-rulings amendment): the "district" nav-stack screen
## (T-1138's windowed drill-down, a separate nav.push() hop from "regional")
## RETIRES as a nav-stack level — the continuous cursor-anchored zoom ladder
## means descent/ascent through every rung (Region -> District -> Quarter)
## happens INSIDE the "regional" screen via zoom, not by pushing a new
## screen. D-013 "the zoom gesture owns spatial descent" is restored for
## this seam (Jeroen's ruling) — descent is a continuous gesture, not a
## discrete nav hop. Esc from anywhere in the ladder is therefore a single
## nav.pop() back to "system", exactly the same _handle_key() KEY_ESCAPE
## branch every other non-"reach" screen already uses; there is no longer a
## "district" screen entry in the match/registration table.
signal economics_link_requested(system_id: String)
var _systems: Array = []
var _system_lookup: Dictionary = {} # system_id → system dict
var _reach_screen = null # ReachScreen
var _system_screen = null # SystemScreen
var _planet_screen = null # PlanetScreen
var _regional_screen = null # RegionalScreen — now the whole zoom ladder (T-1153)
func _ready() -> void:
manifest = load("res://ui/implant/apps/atlas/app.tres")
super._ready()
func on_install() -> void:
_load_system_data()
# T-949: the star map now arrives over the bridge, possibly after this app
# is already installed (or before the handshake completes at all) — refresh
# once it lands instead of assuming _load_system_data's first call has data.
SimBridge.star_map_received.connect(_on_star_map_received)
var implant_theme = load("res://ui/implant/default_implant.tres")
_reach_screen = ReachScreen.new()
_reach_screen.setup(implant_theme)
_reach_screen.set_systems(_systems, _system_lookup)
_reach_screen.system_selected.connect(_on_system_selected)
register_screen("reach", _reach_screen)
_system_screen = SystemScreen.new()
_system_screen.setup(implant_theme)
_system_screen.set_systems(_systems)
_system_screen.body_selected.connect(_on_body_selected)
register_screen("system", _system_screen)
_planet_screen = PlanetScreen.new()
_planet_screen.setup(implant_theme)
register_screen("planet", _planet_screen)
_regional_screen = RegionalScreen.new()
_regional_screen.back_requested.connect(_on_regional_back)
register_screen("regional", _regional_screen)
nav.set_default("reach")
func on_open(_mode: int) -> void:
# Base class handles nav.push_default() on first open.
if current_screen_id() == "reach" and _reach_screen:
_reach_screen.refresh_info_panel_visibility()
func _unhandled_key_input(event: InputEvent) -> void:
if not event is InputEventKey:
return
if manifest == null or not HudGroups.is_app_active(manifest.app_path):
return
if not event.is_pressed() or event.is_echo():
return
# "regional" (the whole stepped zoom ladder, StepCanvasViewer, T-1182)
# handles its own Esc via _gui_input — a stray M/other unhandled key on
# that screen must not ALSO fire this app's own _handle_key underneath
# the viewer's own handling.
if current_screen_id() == "regional":
return
_handle_key(event as InputEventKey)
get_viewport().set_input_as_handled()
func _handle_key(event: InputEventKey) -> void:
match event.physical_keycode:
KEY_M:
HudGroups.close_app()
KEY_ESCAPE:
var has_open_panel: bool = _system_screen and (
_system_screen.has_body_panel_open() or _system_screen.has_station_panel_open()
)
if current_screen_id() == "system" and has_open_panel:
_system_screen.close_panels()
elif current_screen_id() == "reach":
HudGroups.close_app()
else:
nav.pop()
KEY_ENTER, KEY_KP_ENTER:
_handle_enter()
KEY_BRACKETLEFT:
if current_screen_id() == "system" and _system_screen:
_system_screen.navigate_system(-1)
KEY_BRACKETRIGHT:
if current_screen_id() == "system" and _system_screen:
_system_screen.navigate_system(1)
func _handle_enter() -> void:
match current_screen_id():
"reach":
if _reach_screen and _reach_screen.has_selection():
_reach_screen.trigger_enter()
"system":
if _system_screen and not _system_screen.is_in_orbital():
var sys: Dictionary = _system_screen.current_system()
nav.replace("system", {"mode": "orbital", "system": sys})
elif _system_screen and _system_screen.has_body_panel_open():
var body: Dictionary = _system_screen.get_selected_body()
if body.get("terrain_reference") != null:
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
})
# =============================================================================
# Signal handlers
# =============================================================================
func _on_system_selected(system_id: String) -> void:
var idx: int = _system_idx_by_id(system_id)
if _system_screen:
_system_screen.set_selected_idx(idx)
var system: Dictionary = _system_lookup.get(system_id, {"system_id": system_id})
nav.push("system", {"mode": "orbital", "system": system})
func _on_body_selected(body: Dictionary) -> void:
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
})
func _on_regional_back() -> void:
nav.pop()
## T-949: the star map arrived — cache it, rebuild the local system
## list/lookup, and push it into whichever screens already exist. set_systems()
## is safe to call again after initial setup (ReachScreen/SystemScreen both
## just recompute their layout from the new data).
func _on_star_map_received(response: Dictionary) -> void:
SystemIndex.ingest(response)
_load_system_data()
if _reach_screen:
_reach_screen.set_systems(_systems, _system_lookup)
if _system_screen:
_system_screen.set_systems(_systems)
# =============================================================================
# Helpers
# =============================================================================
func _system_idx_by_id(system_id: String) -> int:
for i: int in range(_systems.size()):
if _systems[i].get("system_id", "") == system_id:
return i
return 0
func _load_system_data() -> void:
SystemIndex.request_refresh()
_systems = SystemIndex.get_sorted_systems()
_system_lookup.clear()
for node: Dictionary in _systems:
_system_lookup[node.get("system_id", "")] = node