|
|
|
@@ -17,19 +17,18 @@ extends Control
|
|
|
|
|
##
|
|
|
|
|
## Design notes (mirroring AtlasViewer's own split, D-226 §5, extended T-1153):
|
|
|
|
|
## - _canvas (Node2D) holds AtlasWindowOverlay; pan = _canvas.position, zoom
|
|
|
|
|
## = _canvas.scale.
|
|
|
|
|
## - Zoom is client-side on the ALREADY-HELD composite frame-to-frame, but
|
|
|
|
|
## CONTINUOUS AND UNCLAMPED ACROSS RUNGS (D-013): crossing a rung's
|
|
|
|
|
## coverage ceiling (§5, AtlasWindowGeometry.select_rung()) fires a
|
|
|
|
|
## background request for the new granularity while the OLD composite
|
|
|
|
|
## 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.
|
|
|
|
|
## = _canvas.scale. Zoom is client-side on the ALREADY-HELD composite
|
|
|
|
|
## frame-to-frame, but CONTINUOUS AND UNCLAMPED ACROSS RUNGS (D-013):
|
|
|
|
|
## crossing a rung's coverage ceiling (§5, AtlasWindowGeometry.
|
|
|
|
|
## select_rung()) fires a background request for the new granularity
|
|
|
|
|
## while the OLD composite 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()) — 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.
|
|
|
|
|
## - _window_request owns the single-window cache/debounce/retry; _tile_set
|
|
|
|
|
## owns N of those for the tiled rest state — this Control decides WHICH.
|
|
|
|
|
##
|
|
|
|
|
## Navigation (Jeroen's input-model ruling: LMB-drag panning BREAKS click
|
|
|
|
|
## semantics with future map objects, so it's removed entirely):
|
|
|
|
@@ -49,38 +48,36 @@ const OVERLAY_BAR_HEADER_RESERVE: float = 360.0
|
|
|
|
|
## (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 (would
|
|
|
|
|
## silently show LESS than the whole body). 0.0005 covers a ~120,000 km-
|
|
|
|
|
## radius body at a 3840px 4K viewport.
|
|
|
|
|
## MIN_ZOOM must stay low enough that enter_orbital()'s largest legal `n`
|
|
|
|
|
## COVER-fits without clamping (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
|
|
|
|
|
|
|
|
|
|
## T-1145 item 2: WASD/arrow-key continuous pan speed, in CANVAS px/s at
|
|
|
|
|
## zoom=1.0 — actual screen-space rate is this times CURRENT _view_zoom, so
|
|
|
|
|
## panning covers the same TERRAIN per second regardless of zoom level.
|
|
|
|
|
## ~6 districts/s at zoom=1.0 (96/16) — brisk, not a crawl.
|
|
|
|
|
## T-1145 item 2: WASD/arrow-key pan speed, CANVAS px/s at zoom=1.0 — actual
|
|
|
|
|
## screen-space rate scales by CURRENT _view_zoom, so panning covers the same
|
|
|
|
|
## TERRAIN/s regardless of zoom. ~6 districts/s at zoom=1.0 (96/16) — brisk.
|
|
|
|
|
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"). Uses the SAME speed as WASD (one pan feel,
|
|
|
|
|
## two triggers) — no separate constant, _process() reads PAN_SPEED_CANVAS_PX_S for both.
|
|
|
|
|
## T-1145 item 2: cursor-to-edge distance (px) that triggers edge-scroll
|
|
|
|
|
## (Jeroen's "~24px"). Same speed as WASD — _process() reads
|
|
|
|
|
## PAN_SPEED_CANVAS_PX_S for both, no separate constant.
|
|
|
|
|
const EDGE_SCROLL_MARGIN_PX: float = 24.0
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
## native pixel size; this constant IS the native size). 16px/cell at n=64
|
|
|
|
|
## gives a ~1024px-wide composite before zoom, comfortably inside a
|
|
|
|
|
## 1280x720+ viewport at the DEFAULT_N=32 interactive default (512px) too.
|
|
|
|
|
## (unlike AtlasViewer's heightmap, no source texture dictates a native pixel
|
|
|
|
|
## size; this constant IS the native size). 16px/cell at n=64 gives a
|
|
|
|
|
## ~1024px-wide composite before zoom, comfortable in a 1280x720+ viewport.
|
|
|
|
|
const CELL_PIXEL_SIZE: float = 16.0
|
|
|
|
|
|
|
|
|
|
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 — a dimmer/
|
|
|
|
|
## less-certain read of the same planetary data, not a different visual language.
|
|
|
|
|
## reads as "real data seen through", not a placeholder block — a dimmer/
|
|
|
|
|
## less-certain read of the same planetary data, not a different visual
|
|
|
|
|
## language. Also read by AtlasWindowOverlay for a per-tile pending wash in
|
|
|
|
|
## the mosaic (cold-start dossier) — same "still working" cue, one color.
|
|
|
|
|
const COLOR_BORDER_FADE: Color = Color(0.20, 0.24, 0.30, 0.55)
|
|
|
|
|
|
|
|
|
|
## T-1153/R6: pending-refinement wash — border-fade's referent repointed to
|
|
|
|
@@ -236,11 +233,10 @@ func _exit_tree() -> void:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Enter the window screen centered on `district_center` at District
|
|
|
|
|
## granularity. n defaults to 32. Thin wrapper over _enter_at_rung()
|
|
|
|
|
## (T-1153); survives as a direct-call test entry.
|
|
|
|
|
##
|
|
|
|
|
## T-1142: `district_center` is canonicalized BEFORE it becomes
|
|
|
|
|
## `_held_center` — matching the server's normalize_window_center().
|
|
|
|
|
## granularity. n defaults to 32. Thin wrapper over _enter_at_rung() (T-1153);
|
|
|
|
|
## survives as a direct-call test entry. T-1142: `district_center` is
|
|
|
|
|
## canonicalized BEFORE it becomes `_held_center`, matching the server's
|
|
|
|
|
## normalize_window_center().
|
|
|
|
|
func enter(
|
|
|
|
|
body: Dictionary,
|
|
|
|
|
system: Dictionary,
|
|
|
|
@@ -313,11 +309,10 @@ func _enter_tile_mode(body: Dictionary, system: Dictionary, radius_km: float) ->
|
|
|
|
|
|
|
|
|
|
## Shared entry path for enter()/enter_orbital() (T-1153) — `district_center`
|
|
|
|
|
## must already be canonicalized by the caller. Resets every piece of
|
|
|
|
|
## held/request state for a fresh descent, plus _held_granularity_v2.
|
|
|
|
|
##
|
|
|
|
|
## **C1 clamp-mirror, one layer up:** `n` MUST be clamped via
|
|
|
|
|
## `_clamp_window_n_mirror_v2()` BEFORE it becomes `_held_n` — mirroring
|
|
|
|
|
## AtlasWindowRequest.request_now()'s own clamp (PR #191 Tyre C1).
|
|
|
|
|
## held/request state for a fresh descent, plus _held_granularity_v2. `n`
|
|
|
|
|
## MUST be clamped via `_clamp_window_n_mirror_v2()` BEFORE it becomes
|
|
|
|
|
## `_held_n` (C1 clamp-mirror, one layer up) — mirroring AtlasWindowRequest.
|
|
|
|
|
## request_now()'s own clamp (PR #191 Tyre C1).
|
|
|
|
|
func _enter_at_rung(
|
|
|
|
|
body: Dictionary,
|
|
|
|
|
system: Dictionary,
|
|
|
|
@@ -422,6 +417,13 @@ func get_body_radius_km() -> float:
|
|
|
|
|
return float(_body.get("body_radius_km", 0.0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Cold-start dossier (BUG 2): RegionalScreen.enter() reads this to skip a
|
|
|
|
|
## redundant enter_orbital() for a repeat nav.push("regional") on the SAME
|
|
|
|
|
## body — see that call site's own doc for why.
|
|
|
|
|
func get_body_id() -> String:
|
|
|
|
|
return _dict_str(_body, "body_id", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## District-cell pixel size at zoom=1.0 — AtlasWindowOverlay reads this
|
|
|
|
|
## rather than hardcoding CELL_PIXEL_SIZE itself, so the viewer stays the
|
|
|
|
|
## single source of geometry truth (same "viewer owns the transform, overlay
|
|
|
|
@@ -466,12 +468,10 @@ func _on_window_ready(window: Dictionary) -> void:
|
|
|
|
|
# showing — AtlasWindowRequest already filtered by its own last-asked
|
|
|
|
|
# (center, n, granularity_v2) via the echo (§2/T-1150/T-1152), but a
|
|
|
|
|
# cache-hit path can fire synchronously from enter() before _held_center
|
|
|
|
|
# is what the signal handler expects in a re-entrant call; comparing
|
|
|
|
|
# again here is cheap and removes any ordering assumption between
|
|
|
|
|
# enter()'s two calls. granularity_v2 (T-1153) is compared too — a
|
|
|
|
|
# district-rung response answering a request that's SINCE moved on to a
|
|
|
|
|
# region-rung request (rapid wheel-zoom) must not be adopted just because
|
|
|
|
|
# center/n happen to still match.
|
|
|
|
|
# reflects a re-entrant call, so comparing again here removes any
|
|
|
|
|
# ordering assumption. granularity_v2 (T-1153) is compared too — a
|
|
|
|
|
# district response answering a request since moved on to Region (rapid
|
|
|
|
|
# wheel-zoom) must not be adopted just because center/n still match.
|
|
|
|
|
var w_center := _vec_from_center(window.get("center", [0, 0]))
|
|
|
|
|
var w_n := int(window.get("n", 0))
|
|
|
|
|
var w_granularity_v2 := str(
|
|
|
|
@@ -513,11 +513,11 @@ func _on_tile_ready(_index: int) -> void:
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# View transform (mirrors AtlasViewer's own — pan is real; zoom is CURSOR-
|
|
|
|
|
# ANCHORED and CONTINUOUS ACROSS RUNGS (T-1153, D-226 T-1143-rulings
|
|
|
|
|
# amendment): the held composite is always drawn client-side-zoomed with NO
|
|
|
|
|
# re-request, but crossing a rung's spacing threshold fires a NEW request at
|
|
|
|
|
# the new granularity in the background (progressive refinement — see
|
|
|
|
|
# _maybe_reselect_rung()'s own doc) while the OLD composite stays on screen.
|
|
|
|
|
# ANCHORED and CONTINUOUS ACROSS RUNGS, D-226 T-1143-rulings: the held
|
|
|
|
|
# composite is always drawn client-side-zoomed with NO re-request, but
|
|
|
|
|
# crossing a rung's spacing threshold fires a background request at the new
|
|
|
|
|
# granularity (progressive refinement, see _maybe_reselect_rung()) while the
|
|
|
|
|
# OLD composite stays on screen.
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -561,16 +561,13 @@ func _current_world_extent_m() -> float:
|
|
|
|
|
## §5 rung-selection rule + progressive refinement (T-1153): after a zoom
|
|
|
|
|
## step, recompute the legal rung for the NOW-displayed world extent. If it
|
|
|
|
|
## differs from what's HELD, request the new granularity centered on the
|
|
|
|
|
## CURRENT screen-center (same formula as _maybe_refloat_window()).
|
|
|
|
|
##
|
|
|
|
|
## **C1 clamp-mirror, a THIRD layer up:** `_held_n` MUST be re-clamped via
|
|
|
|
|
## `_clamp_window_n_mirror_v2()` for the TARGET rung — a stale large
|
|
|
|
|
## `_held_n` desyncs `_on_window_ready()`'s staleness check.
|
|
|
|
|
##
|
|
|
|
|
## Progressive refinement: does NOT touch `_window`/`_held_granularity_v2` —
|
|
|
|
|
## the OLD composite keeps drawing until _on_window_ready() adopts the new
|
|
|
|
|
## one (§6 "no mode flip"). Live round 5: this lag is what made
|
|
|
|
|
## `_maybe_reset_to_canonical_frame()`'s OLD guard misfire.
|
|
|
|
|
## CURRENT screen-center (same formula as _maybe_refloat_window()). `_held_n`
|
|
|
|
|
## MUST be re-clamped via `_clamp_window_n_mirror_v2()` for the TARGET rung
|
|
|
|
|
## (C1 clamp-mirror, a third layer up) — a stale large `_held_n` desyncs
|
|
|
|
|
## `_on_window_ready()`'s staleness check. Does NOT touch
|
|
|
|
|
## `_window`/`_held_granularity_v2` — the OLD composite keeps drawing until
|
|
|
|
|
## _on_window_ready() adopts the new one (§6 "no mode flip"; live round 5:
|
|
|
|
|
## this lag is what made `_maybe_reset_to_canonical_frame()`'s OLD guard misfire).
|
|
|
|
|
func _maybe_reselect_rung() -> void:
|
|
|
|
|
if _held_n <= 0:
|
|
|
|
|
return
|
|
|
|
@@ -660,20 +657,18 @@ func _is_at_canonical_frame() -> bool:
|
|
|
|
|
## fires only on the transition INTO fully-zoomed-out from non-canonical.
|
|
|
|
|
##
|
|
|
|
|
## **Live round 5 fix:** the old guard checked only
|
|
|
|
|
## `_held_center`/`_held_granularity_v2` — a LAGGING field (updated only on
|
|
|
|
|
## response adoption). A TILING body's granularity stays stale "Region"
|
|
|
|
|
## after zooming IN leaves tile mode, misreading "already canonical" and
|
|
|
|
|
## never resetting. Fixed by also requiring `is_tile_mode()` to match.
|
|
|
|
|
## `_held_center`/`_held_granularity_v2` (LAGGING, updated only on response
|
|
|
|
|
## adoption) — a TILING body's granularity stayed stale "Region" after
|
|
|
|
|
## zooming IN left tile mode, misreading "already canonical." Fixed by also
|
|
|
|
|
## requiring `is_tile_mode()` to match.
|
|
|
|
|
##
|
|
|
|
|
## **Live round 6 fix (round 5's SECOND fix overshot into a storm):** a
|
|
|
|
|
## per-tick zoom-equality check on THIS guard made it LEVEL-triggered —
|
|
|
|
|
## continued zoom-out kept nudging `_view_zoom` below fit, so the guard
|
|
|
|
|
## read "not already there" every tick and `enter_orbital()` fired
|
|
|
|
|
## **Live round 6 fix (round 5's fix overshot into a storm):** a per-tick
|
|
|
|
|
## zoom-equality check made this guard LEVEL-triggered — continued zoom-out
|
|
|
|
|
## kept nudging `_view_zoom` below fit, so `enter_orbital()` fired
|
|
|
|
|
## repeatedly: tile set torn down/recreated each time, orphaning in-flight
|
|
|
|
|
## responses (nothing held → black), flooding the server (889/897 wire
|
|
|
|
|
## responses in one zoom-out phase). Fixed by moving the zoom-drift concern
|
|
|
|
|
## to `_zoom_at()`'s own zoom floor instead — this guard's
|
|
|
|
|
## mode/center/granularity check alone stays edge-triggered.
|
|
|
|
|
## responses (nothing held -> black), flooding the server (889/897 wire
|
|
|
|
|
## responses in one phase). Fixed by moving the zoom-drift concern to
|
|
|
|
|
## `_zoom_at()`'s own floor — this guard stays edge-triggered.
|
|
|
|
|
func _maybe_reset_to_canonical_frame() -> bool:
|
|
|
|
|
var radius_km: float = float(_body.get("body_radius_km", 0.0))
|
|
|
|
|
if radius_km <= 0.0:
|
|
|
|
@@ -711,7 +706,6 @@ func set_view(zoom: float, offset: Vector2) -> void:
|
|
|
|
|
## After a pan delta (T-1145: WASD/edge-scroll), check whether the
|
|
|
|
|
## screen-center now maps to a DistrictPos outside the held window's extent
|
|
|
|
|
## — if so, float a NEW window centered on that point via the debounced path.
|
|
|
|
|
##
|
|
|
|
|
## T-1142 (item 6a): the edge-crossing decision is computed in RAW absolute
|
|
|
|
|
## district space — only the FINAL new_center is canonicalized, matching the
|
|
|
|
|
## server's normalize_window_center().
|
|
|
|
@@ -767,29 +761,23 @@ func _maybe_refloat_window() -> void:
|
|
|
|
|
func _draw() -> void:
|
|
|
|
|
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
|
|
|
|
|
if _tile_mode:
|
|
|
|
|
# T-1153: single-window border-fade/pending-wash don't apply to a
|
|
|
|
|
# mosaic — AtlasWindowOverlay's tile draw only paints arrived tiles;
|
|
|
|
|
# an unarrived one is an honest gap over COLOR_BG, no separate fade.
|
|
|
|
|
# T-1153: this viewer's own border-fade/pending-wash are single-window
|
|
|
|
|
# concepts (one extent, one state) — a mosaic's per-tile pending wash
|
|
|
|
|
# is drawn by AtlasWindowOverlay itself (see _draw_tile_mosaic()).
|
|
|
|
|
return
|
|
|
|
|
if _window == null:
|
|
|
|
|
# §5 "what renders during the wait": a border-fade to the underlying
|
|
|
|
|
# whole-body context rather than black/a spinner. This viewer has no
|
|
|
|
|
# resident whole-body texture of its own — the honest available
|
|
|
|
|
# substitute is a dim fade wash over the held composite's last-known
|
|
|
|
|
# extent, reusing the request object's own is_pending() for the
|
|
|
|
|
# "still working" cue rather than new dressing.
|
|
|
|
|
# whole-body context rather than black/a spinner — the honest
|
|
|
|
|
# available substitute, this viewer having no resident whole-body
|
|
|
|
|
# texture of its own.
|
|
|
|
|
_draw_border_fade()
|
|
|
|
|
elif _window_request and _window_request.is_pending():
|
|
|
|
|
# T-1153/R6: the border-fade's REFERENT repointed — a rung-crossing
|
|
|
|
|
# zoom (progressive refinement) leaves `_window` non-null (the OLD
|
|
|
|
|
# composite is still the thing on screen, drawn by AtlasWindowOverlay
|
|
|
|
|
# as always) while a NEW rung's request is in flight underneath it.
|
|
|
|
|
# R6's ruling: "the mechanism survives; its target must be repointed
|
|
|
|
|
# to 'the previous derived composite at this position'" — exactly
|
|
|
|
|
# this case. A lighter pending wash (not the full opaque fade the
|
|
|
|
|
# no-composite-at-all case uses, since there IS real data showing
|
|
|
|
|
# through here, not emptiness) signals "sharper detail incoming"
|
|
|
|
|
# without implying the current view is stale or wrong.
|
|
|
|
|
# composite still on screen) while a NEW rung's request is in flight
|
|
|
|
|
# underneath it. A lighter pending wash (not the full opaque fade the
|
|
|
|
|
# no-composite-at-all case uses, since real data IS showing through)
|
|
|
|
|
# signals "sharper detail incoming" without implying the view is wrong.
|
|
|
|
|
_draw_pending_refinement_wash()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -885,9 +873,18 @@ func _handle_key(event: InputEventKey) -> void:
|
|
|
|
|
## T-1145 item 2: continuous WASD/arrow-key pan + edge-scroll, both HELD-state
|
|
|
|
|
## effects polled every frame, handed to _apply_pan_delta() (split out for
|
|
|
|
|
## testability). Skips while hidden (screen not the active nav-stack entry).
|
|
|
|
|
## Cold-start self-heal (coordinator live repro): every queue_redraw() site
|
|
|
|
|
## is already paired with _overlay_node's own, yet a cold server's slow first
|
|
|
|
|
## AnalyzeBody still showed a black mosaic with every tile held/textured,
|
|
|
|
|
## only resolving on an unrelated gesture. Rather than chase one signal edge
|
|
|
|
|
## that might drop, redraw every frame while any tile is pending — free once
|
|
|
|
|
## complete.
|
|
|
|
|
func _process(delta: float) -> void:
|
|
|
|
|
if not visible:
|
|
|
|
|
return
|
|
|
|
|
if _tile_mode and _tile_set and _tile_set.has_pending_tiles():
|
|
|
|
|
queue_redraw()
|
|
|
|
|
_overlay_node.queue_redraw()
|
|
|
|
|
var direction: Vector2 = _held_pan_direction()
|
|
|
|
|
if _is_cursor_edge_scrolling():
|
|
|
|
|
direction += _edge_scroll_direction()
|
|
|
|
|