The atlas 'regional' screen now opens the LADDER at the canonical orbital frame (Region granularity, whole body fitted and centered) and wheel zoom descends continuously — cursor-anchored, unclamped across rungs, with progressive refinement (held composite keeps drawing, finer rung swaps in place on arrival; no blank frame, no mode flip). Full-zoom-out resets to the canonical planetary frame per Jeroen's HARD condition (is_fully_zoomed_out = extent >= body circumference, not a zoom-value heuristic). The district_screen nav hop is deleted — D-013 restored: descent is a zoom gesture, not a nav push. AtlasViewer's heightmap-texture path is unreachable from nav (code intact; overlay surface deferred, see report/tickets). Rung selection: design doc §5's literal formula has NO legal District band at any real viewport (visual-tolerance band and n=64 coverage ceiling never overlap — pinned by executable boundary tests at 1600x900); select_rung() splits it into a coverage ceiling (decides Region) then the 2x visual tolerance (District vs Quarter), documented at the function. In practice the ladder steps Region -> Quarter directly. Wire: window_granularity_v2 encoded (omitted at District for byte-compat), granularity_v2 echoed value keyed + staleness-guarded end to end; Region clamp mirror replicates the server's bounded halving loop (no closed form). MIN/MAX_ZOOM widened to [0.0005, 64] — the old 0.5 floor would have clamped a real body's canonical fit zoom, violating the reset condition. Real pre-existing bug fixed in atlas_window_overlay.gd: the draw path used echoed n as both cell-grid dimension and district extent — only coincidentally correct at District granularity; Quarter/Region would have read wrong array offsets. cell_grid_side_for_window() now mirrors the server's WindowGranularity::cell_grid_side. Tests: +26 pure-function geometry tests, new 30-test zoom-ladder suite, extensions across the window cache/request/overlay/delivery suites. Full suite 3518 green; cold-parse clean.
362 lines
18 KiB
GDScript
362 lines
18 KiB
GDScript
## T-1153 (D-226 T-1143-rulings amendment): tests for the continuous
|
|
## cursor-anchored zoom ladder — enter_orbital() (the canonical planetary
|
|
## frame), progressive refinement (held composite survives a rung-crossing
|
|
## request), the full-zoom-out reset (Jeroen's HARD condition), rung
|
|
## reselection on zoom, and E/W wrap + pole-wall clamps at Region
|
|
## granularity. Split out of test_atlas_window_viewer.gd (which owns the
|
|
## pre-T-1153 window-viewer behavior — entry, cache reuse, WASD/edge-scroll,
|
|
## fit-and-center) purely for file-length reasons (gdlint max-file-lines);
|
|
## same instantiation/mock-response conventions as that file, not a
|
|
## different testing philosophy.
|
|
class_name TestAtlasZoomLadder
|
|
extends GdUnitTestSuite
|
|
|
|
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
|
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
|
|
|
|
|
## Build a hand-authored DistrictWindowLayer dict (n=2 by default) — mirrors
|
|
## test_atlas_window_viewer.gd's own _mock_window().
|
|
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
|
|
return {
|
|
"center": [center.x, center.y],
|
|
"n": n,
|
|
"morphology": PackedByteArray([8, 14, 0, 1]),
|
|
"elev_q": PackedByteArray([40, 90, 5, 60]),
|
|
"temp_dc": [120, 95, -32768, 60],
|
|
"moisture_q": PackedByteArray([50, 30, 90, 20]),
|
|
"vegetation": PackedByteArray([2, 1, 6, 3]),
|
|
"glaciation": PackedByteArray([0, 0, 1, 2]),
|
|
}
|
|
|
|
|
|
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
|
|
return {"body_id": body_id, "status": "Ready", "district_window": window}
|
|
|
|
|
|
# =============================================================================
|
|
# T-1153: enter_orbital() — the canonical planetary frame, the ladder's TOP
|
|
# REST STATE (Jeroen's HARD condition, D-226 T-1143-rulings amendment).
|
|
# =============================================================================
|
|
|
|
|
|
## enter_orbital() must center on district (0,0) — "district (0,0) sits at
|
|
## lon 0 / the equator" (AtlasDescendGeometry's own doc).
|
|
func test_enter_orbital_centers_on_the_canonical_origin() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_that(v._held_center).is_equal(Vector2i.ZERO)
|
|
|
|
|
|
## enter_orbital() must request at Region granularity — the orbital view IS
|
|
## the Region rung at high n, not a separate screen/mode (the ticket's own
|
|
## framing).
|
|
func test_enter_orbital_requests_region_granularity() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v._held_granularity_v2).is_equal("Region")
|
|
|
|
|
|
## enter_orbital()'s `n` must equal the body's full equatorial circumference
|
|
## in districts (district_extent().cols) — the whole body fitted to the
|
|
## canvas, per Jeroen's HARD condition wording.
|
|
func test_enter_orbital_n_covers_the_full_circumference() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
var radius_km := 6238.4
|
|
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
|
assert_int(v._held_n).is_equal(int(extent["cols"]))
|
|
|
|
|
|
## A no-radius body (tiny test body) has no circumference concept —
|
|
## enter_orbital() falls back to the District-rung default window rather
|
|
## than crashing or deriving a degenerate n.
|
|
func test_enter_orbital_no_radius_body_falls_back_to_district() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter_orbital({"body_id": "GJ380c"}, {})
|
|
assert_str(v._held_granularity_v2).is_equal("District")
|
|
assert_int(v._held_n).is_equal(AtlasWindowRequest.DISTRICT_WINDOW_DEFAULT_N)
|
|
|
|
|
|
# =============================================================================
|
|
# T-1153: progressive refinement — the held composite survives until the
|
|
# replacement arrives (§6 "no mode flip": never a blank frame, never a
|
|
# clear-then-redraw).
|
|
# =============================================================================
|
|
|
|
|
|
## The core acceptance test: once a window is held, a request for a
|
|
## DIFFERENT rung being in-flight must NOT clear `_window` — the old
|
|
## composite stays exactly what get_district_window() returns until the new
|
|
## rung's response actually arrives and is adopted.
|
|
func test_held_window_survives_while_a_different_rung_request_is_in_flight() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
|
|
assert_that(v.get_district_window()).is_equal(district_window)
|
|
|
|
# Simulate a rung-reselect firing a NEW (Region) request without the
|
|
# response having arrived yet — direct call, mirroring what
|
|
# _maybe_reselect_rung() does internally.
|
|
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
|
|
|
|
assert_that(v.get_district_window()).override_failure_message(
|
|
"the OLD composite must survive while a different-rung request is in"
|
|
+ " flight — no blank frame, no premature clear"
|
|
).is_equal(district_window)
|
|
|
|
|
|
## Once the new rung's response actually arrives (matching the CURRENTLY
|
|
## in-flight request's granularity_v2), it swaps in — the composite reference
|
|
## changes from the old rung's window to the new one.
|
|
func test_new_rung_window_swaps_in_once_it_arrives() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
|
|
|
|
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
|
|
var region_window: Dictionary = {
|
|
"center": [10, 20], "n": 2, "granularity_v2": "Region",
|
|
"morphology": PackedByteArray([1, 2, 3, 4]),
|
|
"elev_q": PackedByteArray([10, 20, 30, 40]),
|
|
"temp_dc": [0, 0, 0, 0],
|
|
"moisture_q": PackedByteArray([0, 0, 0, 0]),
|
|
"vegetation": PackedByteArray([0, 0, 0, 0]),
|
|
"glaciation": PackedByteArray([0, 0, 0, 0]),
|
|
}
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", region_window))
|
|
|
|
assert_that(v.get_district_window()).override_failure_message(
|
|
"once the new rung's matching response arrives, it must swap in"
|
|
).is_equal(region_window)
|
|
assert_str(v._held_granularity_v2).is_equal("Region")
|
|
|
|
|
|
## A response for a rung OTHER than what's currently requested (e.g. a
|
|
## District response arriving after the viewer has already moved on to a
|
|
## Region request — a rapid wheel-zoom race) must be discarded as stale, the
|
|
## held composite untouched.
|
|
func test_stale_rung_response_after_moving_on_is_discarded() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
|
|
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
|
|
|
|
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
|
|
# A LATE district-rung response for the same (center, n) arrives after the
|
|
# viewer has already moved on to requesting Region — must be dropped.
|
|
var late_district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
|
|
late_district_window["morphology"] = PackedByteArray([9, 9, 9, 9]) # distinguishable payload
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", late_district_window))
|
|
|
|
assert_that(v.get_district_window()).override_failure_message(
|
|
"a stale response for a rung the viewer has since moved on from must be discarded"
|
|
).is_equal(district_window)
|
|
|
|
|
|
# =============================================================================
|
|
# T-1153: full-zoom-out reset (Jeroen's HARD condition).
|
|
# =============================================================================
|
|
|
|
|
|
## Directly at the canonical frame already (center (0,0), Region granularity)
|
|
## must be a no-op — never re-fights a player zooming back IN from the top.
|
|
func test_reset_to_canonical_frame_is_noop_when_already_there() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._held_center = Vector2i.ZERO
|
|
v._held_granularity_v2 = "Region"
|
|
var fired: bool = v._maybe_reset_to_canonical_frame()
|
|
assert_bool(fired).override_failure_message(
|
|
"already at the canonical frame — the reset must not re-fire"
|
|
).is_false()
|
|
|
|
|
|
## A no-radius body must never trigger the reset (no circumference concept —
|
|
## matches enter_orbital()'s own guard).
|
|
func test_reset_to_canonical_frame_never_fires_for_no_radius_body() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(5, 5), 2)
|
|
var fired: bool = v._maybe_reset_to_canonical_frame()
|
|
assert_bool(fired).is_false()
|
|
|
|
|
|
## Away from the canonical frame (a drifted District-rung pan/zoom state)
|
|
## with a fully-zoomed-out world extent must reset — the direct wiring test
|
|
## for Jeroen's HARD condition: enter() at a far-off center, then force the
|
|
## view zoom low enough that the displayed extent covers the whole body.
|
|
func test_reset_to_canonical_frame_fires_and_re_centers_when_fully_zoomed_out() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
var radius_km := 50.0 # tiny synthetic body — small circumference, reachable by a modest zoom-out
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(500, 10), 32)
|
|
# Force a very low zoom — a huge displayed world extent, comfortably over
|
|
# this tiny body's whole circumference.
|
|
v._view_zoom = AtlasWindowViewer.MIN_ZOOM
|
|
|
|
var fired: bool = v._maybe_reset_to_canonical_frame()
|
|
|
|
assert_bool(fired).override_failure_message(
|
|
"a fully-zoomed-out view on a real-radius body must trigger the reset"
|
|
).is_true()
|
|
assert_that(v._held_center).override_failure_message(
|
|
"the reset must re-center on the canonical origin (0,0)"
|
|
).is_equal(Vector2i.ZERO)
|
|
assert_str(v._held_granularity_v2).override_failure_message(
|
|
"the reset must land on the Region rung — the ladder's top rest state"
|
|
).is_equal("Region")
|
|
|
|
|
|
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
|
|
## reset — only reaching the top of the ladder resets, not every zoom step.
|
|
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(500, 10), 32)
|
|
v._view_zoom = 1.0 # a normal, non-extreme zoom — nowhere near full planetary coverage
|
|
|
|
var fired: bool = v._maybe_reset_to_canonical_frame()
|
|
|
|
assert_bool(fired).override_failure_message(
|
|
"an ordinary District-rung view must not trigger the top-rest-state reset"
|
|
).is_false()
|
|
|
|
|
|
# =============================================================================
|
|
# T-1153: rung reselection — _zoom_at() crossing a rung threshold fires a
|
|
# new request without touching the held composite.
|
|
# =============================================================================
|
|
|
|
|
|
## Zooming OUT far enough from a District-rung window (small n, so a modest
|
|
## zoom-out already covers a huge world extent) must fire a coarser-rung
|
|
## request — the wheel-zoom-driven wiring test for _maybe_reselect_rung().
|
|
func test_zoom_out_past_district_threshold_requests_a_coarser_rung() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 2) # n=2 — a tiny window, easy to overshoot
|
|
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 2)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
|
|
assert_str(v._window_request.get_granularity_v2()).is_equal("District")
|
|
|
|
# A big zoom-OUT factor (well under 1.0) from a tiny n=2 window blows the
|
|
# displayed world extent WAY past District's threshold.
|
|
v._zoom_at(Vector2(400.0, 300.0), 0.01)
|
|
|
|
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
|
|
"zooming out far enough from a small District window must re-request a coarser rung"
|
|
).is_not_equal("District")
|
|
# The OLD composite must still be what's held — progressive refinement,
|
|
# not a block-on-derive clear.
|
|
assert_that(v.get_district_window()).is_equal(district_window)
|
|
|
|
|
|
## Zooming IN on a District-rung window (well within its own legal spacing
|
|
## band) must NOT trigger a rung change — this is the "zoom is client-side on
|
|
## the already-held composite" case, unchanged for in-rung zoom. Sets
|
|
## _view_zoom DIRECTLY to a value inside District's legal band (rather than
|
|
## relying on enter()'s COVER auto-fit, which for a small n can already sit
|
|
## right at Quarter's own threshold — a fit's zoom level is a display-density
|
|
## choice independent of what rung selection would pick from scratch, and
|
|
## this test is specifically about a SINGLE zoom-in STEP not crossing a
|
|
## boundary, not about where the auto-fit itself lands). District's legal
|
|
## band (select_rung()'s own doc: the coverage ceiling and the `2x` visual
|
|
## band only overlap at small viewports — `canvas_px <= DISTRICT_WINDOW_MAX_N
|
|
## * DISTRICT_SPACING_M / 1024 = 128px`) requires a SMALL viewport here,
|
|
## unlike most of this suite's 800x600/1920x1080 fixtures.
|
|
func test_zoom_in_within_district_threshold_does_not_change_rung() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(100.0, 80.0)
|
|
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
|
|
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 32)
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
|
|
v._view_zoom = 0.10666666666666667 # E=120,000m at C=100px — inside District's legal band
|
|
v._apply_transform()
|
|
|
|
v._zoom_at(Vector2(50.0, 40.0), 1.15) # a single ordinary zoom-in step
|
|
|
|
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
|
|
"a single ordinary zoom-in step must not cross a rung threshold"
|
|
).is_equal("District")
|
|
|
|
|
|
# =============================================================================
|
|
# T-1153: E/W wrap and pole-wall clamps at EVERY rung — both are extent-
|
|
# relative (CELL_PIXEL_SIZE-based district-space math, unchanged regardless
|
|
# of which rung's data is actually held), so they must keep working
|
|
# unmodified at Region granularity, not just District/Quarter.
|
|
# =============================================================================
|
|
|
|
|
|
## The pole wall, wired through the real _apply_pan_delta() path, must still
|
|
## clamp at Region granularity — same mechanism as the existing District-rung
|
|
## test (test_wasd_pan_is_clamped_by_the_pole_wall_when_wired), just entered
|
|
## via enter_orbital() instead of enter().
|
|
func test_pole_wall_clamps_at_region_granularity_too() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 800.0)
|
|
var radius_km := 50.0 # tiny synthetic body — pole wall reachable by an ordinary tick
|
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
|
assert_str(v._held_granularity_v2).is_equal("Region")
|
|
|
|
var unclamped_magnitude: float = 500.0 * AtlasWindowViewer.PAN_SPEED_CANVAS_PX_S * v.get_view_zoom()
|
|
v._apply_pan_delta(Vector2(0.0, -1.0), 500.0) # "W"/north held, an absurdly long tick
|
|
|
|
assert_float(absf(v.get_view_offset().y)).override_failure_message(
|
|
"the pole wall must still clamp an extreme pan at Region granularity"
|
|
).is_less(unclamped_magnitude * 0.5)
|
|
|
|
|
|
## East-west wrap (canonicalize_district_center()) must still apply to the
|
|
## pan-edge refloat's resulting center at Region granularity — a pan that
|
|
## carries the screen-center column past the body's circumference must wrap
|
|
## into [0, cols), never run away to an out-of-range column, exactly as the
|
|
## District-rung wrap tests already pin (T-1142 item 6a). At Region's own
|
|
## enormous held_n (a whole circumference), an ORDINARY pan tick's
|
|
## canvas-space delta is negligible relative to the window's half-extent
|
|
## (confirmed: ~0.08 districts per 5-second tick vs. a ~9,772-district
|
|
## half-window) — so this drives _maybe_refloat_window() DIRECTLY off a
|
|
## manually-set _view_offset large enough to genuinely cross the held
|
|
## window's edge, the same "exercise the actual edge-crossing branch, not
|
|
## just its no-op early-return" discipline _maybe_refloat_window()'s own
|
|
## inside-check comment describes.
|
|
func test_pan_edge_refloat_wraps_columns_at_region_granularity_too() -> void:
|
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 800.0)
|
|
var radius_km := 6371.0
|
|
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
|
var cols: int = int(extent["cols"])
|
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
|
assert_str(v._held_granularity_v2).is_equal("Region")
|
|
|
|
# Force the held center to sit one column short of the wrap seam, then
|
|
# shift the CANVAS offset by more than half the window's own on-screen
|
|
# extent — enough to move the screen-center's mapped column past the
|
|
# window's far edge (i.e. past `cols`, crossing the seam) regardless of
|
|
# Region's huge held_n.
|
|
v._held_center = Vector2i(cols - 1, 0)
|
|
var half_window_screen_px: float = float(v._held_n) * v.get_cell_pixel_size() * v.get_view_zoom() * 0.5
|
|
v._view_offset = v.get_view_offset() - Vector2(half_window_screen_px * 1.5, 0.0)
|
|
v._maybe_refloat_window()
|
|
|
|
assert_int(v._held_center.x).override_failure_message(
|
|
"a pan crossing the antimeridian at Region granularity must wrap the"
|
|
+ " resulting center into [0, cols), never run past cols"
|
|
).is_less(cols)
|
|
assert_int(v._held_center.x).is_greater_equal(0)
|